d2l::interactive

Chapter 8 // Architectures

Modern CNNs

A tour of the architectures that won ImageNet, one after another — and the two ideas hidden among them, batch normalization and residual connections, that everything since has been built on.

Adapted from the original chapter of Dive into Deep Learning · CC BY-SA 4.0 · the Python shown is the book's; the figures run a faithful TypeScript port in your browser

Once you can wire a convolution to a pooling layer, the question becomes how to stack them. This chapter is a decade of answers, in the order they arrived — each a brief champion of the ImageNet competition, each keeping the good ideas of the last and adding one. Most of it is architecture: diagrams of blocks. But two of these ideas are real, checkable machinery, and they reshaped deep learning far beyond vision.

Act 1 · The 2012 leap

01/05

LeNet

1995 · sigmoid · avg-pool

5 layers

61.7K params

Meet LeNet, 1995: two conv-and-pool stages, a small classifier, 61.7K parameters. It read cheques for years — then stalled. For a decade, hand-crafted features beat it on real photographs.

AlexNet proved the recipe scales, but it placed every layer by hand and hid most of its weight in two enormous fully connected layers. The next wave made the design modular — and went hunting for that fat.

Act 2 · Design by the block

01/05

one VGG block3×3 conv → ReLU3×3 conv → ReLU2×2 max-pool ↓two 3×3 see a 5×5 window2·9 = 18 weights < 25deeper + narrower, same field, fewer params

VGG stops placing layers one at a time and repeats a block: a few padded 3×3 convs, then a pool. Two stacked 3×3s see the same window as one 5×5 — with fewer weights and an extra nonlinearity.

Blocks made networks deeper, and depth exposed a new problem: the signal flowing through a deep stack drifts and destabilizes, and the gradient flowing back through it dies. The next two ideas are the fixes — and the reason this chapter matters most.

Act 3 · Normalize the middle

01/06

feat 1feat 2feat 3feat 46 examples3.4-2.2-1.3-6.33.6-2.07.8-4.52.0-2.14.3-3.34.1-1.53.8-5.25.0-2.22.9-2.33.8-1.6-5.8-3.8μ 3.66σ 0.91μ -1.96σ 0.27μ 1.96σ 4.36μ -4.23σ 1.31batch

Deep inside a network, a minibatch of activations drifts to wild, mismatched scales — one feature centered at +3, another at −4, spreads from 0.4 to 5. That drift slows every layer above.

Batch normalization steadies the forward signal. But there is a second failure at depth — the backward one — and it needs a different, even simpler idea.

Act 4 · The gradient's highway

01/06

nested function classesF₁⊂F₂⊂…f*if a bigger class contains the smaller,adding layers can only help

Deeper should never be worse: if the extra layers could just copy their input, a deep net would contain the shallow one. But plain deep nets don't find that identity — past a point, adding layers hurts even the training error.

Residual connections and batch normalization together made hundred-layer networks routine. Everything after is variation: what you do with the skip, and who does the designing.

Act 5 · Beyond adding

01/04

dense block · growth = 326496+32128+32160+32192+32each layer's output is concatenated, not addedevery layer sees all earlier feature maps

DenseNettakes ResNet's skip one step further: instead of adding the input back, it concatenatesit. Every layer's output is appended, so channels grow by a fixed growth rate at each step.

The lab

Three claims from the acts, yours to break: that batch norm makes a layer's output deaf to the scale of its input, that a residual skip is the difference between a gradient that survives depth and one that vanishes, and that every one of these architectures is just a traced stack of shapes and parameters.

▸ lab

Batch norm, by hand

Drag the incoming activations off-scale. In training, the batch output holds μ=β, σ=γ no matter what.

incomingafter BN

in μ 3.13 · σ 2.37

out μ 0.00 · σ 1.00

Training: output μ ≈ β, σ ≈ γ — completely deaf to the incoming shift and scale. That invariance is what stabilizes the layers above.

▸ lab

Plain vs residual, at depth

Drag the stack deeper. Watch the plain gradient collapse while the residual one refuses to die.

10²10⁻²10⁻⁶10⁻¹⁰10⁻¹⁴10⁻¹⁸‖grad‖30 blocks deep →
depth30 blocks

plain 1e-17

residual 3.54

ratio 3e+17×

The plain gradient has vanished — the early layers get essentially no signal and can't learn. The residual stack still carries ~1.

▸ lab

The architecture explorer

Pick a network. Every shape and parameter count is traced live by the verified port.

ResNet-18

2015

Residual connections: add the input back, so identity is free. Trains 100+ layers.

11M

parameters

315M

MACs / image

18

weight layers

input 1×96×96 → layers

Conv 7×7 ×64 /264×48×483,200
BatchNorm64×48×48128
ReLU64×48×48·
MaxPool 3×364×24×24·
Residual ×6464×24×2474,112
Residual ×6464×24×2474,112
Residual ×128 /2128×12×12230,528
Residual ×128128×12×12295,680
Residual ×256 /2256×6×6919,808
Residual ×256256×6×61,181,184
Residual ×512 /2512×3×33,674,624
Residual ×512512×3×34,721,664
GlobalAvgPool512·
Flatten512·
Linear ×10105,130

The real code

Every act carries a </> chip with its section's exact PyTorch. The residual block — two idea-bearing lines of it — is the whole chapter in miniature:

resnet.md (PyTorch)
class Residual(nn.Module):
    def __init__(self, num_channels, use_1x1conv=False, strides=1):
        super().__init__()
        self.conv1 = nn.LazyConv2d(num_channels, kernel_size=3, padding=1, stride=strides)
        self.conv2 = nn.LazyConv2d(num_channels, kernel_size=3, padding=1)
        self.conv3 = nn.LazyConv2d(num_channels, kernel_size=1, stride=strides) if use_1x1conv else None
        self.bn1 = nn.LazyBatchNorm2d()
        self.bn2 = nn.LazyBatchNorm2d()
 
    def forward(self, X):
        Y = F.relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
        if self.conv3:
            X = self.conv3(X)
        Y += X                 # the residual connection — everything hinges here
        return F.relu(Y)
Batch normalization from scratch (the other pillar)
batch-norm.md (PyTorch)
def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
    if not torch.is_grad_enabled():
        # prediction: normalize with the running (moving) statistics
        X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
    else:
        # training: statistics of THIS minibatch (dim 0 for FC, (0,2,3) for conv)
        mean = X.mean(dim=0)
        var = ((X - mean) ** 2).mean(dim=0)
        X_hat = (X - mean) / torch.sqrt(var + eps)
        moving_mean = (1.0 - momentum) * moving_mean + momentum * mean
        moving_var = (1.0 - momentum) * moving_var + momentum * var
    Y = gamma * X_hat + beta   # scale and shift
    return Y, moving_mean.data, moving_var.data

Three things to remember

  1. Architectures are stacks of blocks, and the trend is efficiency. After AlexNet, progress meant composing repeatable blocks (VGG), replacing fat fully connected layers with 1×1 convs and global pooling (NiN), and running many kernel sizes in parallel (GoogLeNet) — often with fewer parameters, not more.
  2. Batch normalization stabilizes the forward pass. Normalize each layer's activations to zero mean and unit variance over the minibatch, then let a learned scale and shift back in. It smooths optimization, permits bigger learning rates, and regularizes — and it behaves differently at test time, using running statistics.
  3. Residual connections stabilize the backward pass. Adding the input back around a block means the gradient always has an identity path home, so it never vanishes — which is the only reason networks over 100 layers deep can be trained. Both ideas later became standard far outside vision.

Adapted from Chapter 8 of Dive into Deep Learning by Zhang, Lipton, Li, and Smola (CC BY-SA 4.0). The figures run this site's verified TypeScript port; all computation happens live in your browser.