AI Knowledge Tree

Classic CNN architectures

Trace LeNet, AlexNet, VGG, and ResNet's skip connections, and see why depth without residuals collapses gradient flow.

Image classification went from "hard problem" to "approximately solved" in less than a decade because of four architectures: LeNet (1998) established the convolution-pooling stack; AlexNet (2012) scaled it with ReLUs and GPUs; VGG (2014) showed depth alone helped; ResNet (2015) unlocked truly deep networks with skip connections. Every modern CNN is a refinement of the ResNet pattern.

LeNet (1998)

LeCun's original convolutional network for digit recognition[1]:

input (32x32x1)
  -> conv 5x5, 6 filters    -> relu -> avg pool 2x2
  -> conv 5x5, 16 filters   -> relu -> avg pool 2x2
  -> fc 120 -> relu
  -> fc 84  -> relu
  -> fc 10  -> softmax

Two convolutional blocks, three fully connected layers. About 60K parameters. Trained on MNIST to 99% accuracy. The blueprint for every CNN that followed: stack convolutions, downsample with pooling, flatten, classify.

AlexNet (2012)

Krizhevsky et al.'s ImageNet winner[2]. The "ResNet moment" for the field.

Key changes from LeNet:

  • 5 conv layers + 3 fc layers, ~60M parameters.
  • ReLU instead of sigmoid/tanh (the dead-ReLU lesson).
  • GPU training: two GPUs, splitting the model across them.
  • Dropout in the fc layers.
  • Data augmentation: random crops and horizontal flips.
  • Local response normalization (a precursor to batch norm).

Trained on 1.2M ImageNet images to a top-5 error of 15.3%, halving the previous state of the art. The result triggered the deep-learning boom.

VGG (2014)

Simonyan and Zisserman replaced AlexNet's mix of filter sizes with a uniform stack of 3×33 \times 3 convolutions[3]:

[conv 3x3 x 2] -> pool
[conv 3x3 x 2] -> pool
[conv 3x3 x 3] -> pool
[conv 3x3 x 3] -> pool
[conv 3x3 x 3] -> pool
fc 4096 -> fc 4096 -> fc 1000

Two design choices proved durable:

  • Small filters stacked: two 3×33 \times 3 convolutions have the same receptive field as one 5×55 \times 5, but with fewer parameters and an extra nonlinearity. Three 3×33 \times 3s match a 7×77 \times 7.
  • Channel doubling at each spatial downsample: feature volume stays roughly constant.

VGG-16 and VGG-19 are deeper variants. Reached 7.3% top-5 error on ImageNet. The architecture is heavyweight (138M parameters) but conceptually clean and still used as a feature extractor in many vision papers.

The depth-collapse problem

Going from VGG-19 to "plain VGG-30 or 50" should help: more depth, more capacity, lower training error. Instead, training error increases as plain networks get deeper past ~20 layers.

The diagnosis is not overfitting (training error is the wrong thing for that). It is an optimization problem: gradients propagate poorly through very deep stacks of compositions, and the network gets stuck in a region where it cannot even learn the identity function on each sublayer.

ResNet's contribution was to fix this directly.

ResNet (2015)

He et al.'s residual block[4]:

y=F(x;W)+x.y = F(x; W) + x.

The output is the sum of a learned FF (typically a stack of 2 or 3 convolutions with BatchNorm and ReLU) and a shortcut connection that bypasses FF entirely.

The crucial property: FF now has to learn the difference between the input and the desired output, not the full mapping. If the right answer is the identity (do nothing), FF can be the zero function and the block degenerates to the identity. Training error stops increasing with depth.

A ResNet-50 has 50 layers; ResNet-152 has 152. Both train without collapse and beat VGG-19 substantially on ImageNet.

Why residual works (briefly)

Several explanations, all partially right:

  • Gradient flow: the shortcut connection provides a path that preserves the gradient regardless of how badly FF behaves. Backpropagation through y=x+Fy = x + F gives xˉ=yˉ+yˉ ∂F/∂x\bar x = \bar y + \bar y\,\partial F/\partial x; the yˉ\bar y term passes through unscaled.
  • Loss landscape smoothing: empirical work shows residual nets have much smoother loss landscapes than plain ones.
  • Ensembling: a LL-layer ResNet behaves like an ensemble of paths of varying depths (any subset of residual blocks can be active).

Whichever explanation, the empirical fact is overwhelming: residual connections are the single architectural change that made very deep networks trainable, and they appear in every architecture since (Transformers, U-Nets, diffusion backbones, you name it).

The classic recipe at a glance

YearNetworkLayersParamsTop-5 ImageNet error
1998LeNet-5760K(MNIST, ~1% err)
2012AlexNet860M15.3%
2014VGG-1919144M7.3%
2015ResNet-15215260M3.6%
2017DenseNet-20120120M4.7%
2019EfficientNet-B7~5066M3.5%

ResNet-152 simultaneously went deeper, smaller, and more accurate than VGG-19. The next lesson surveys what came after: DenseNet, EfficientNet, ConvNeXt.

Forward signal strength through depth: a plain deep stack attenuates the signal toward zero, while residual skip connections preserve it, which is why ResNets train at depths plain nets cannot.
Forward signal strength through depth: a plain deep stack attenuates the signal toward zero, while residual skip connections preserve it, which is why ResNets train at depths plain nets cannot.

Check it on arrays

python · numpy
import numpy as np

rng = np.random.default_rng(0)

def conv2d(X, K, stride=1, pad=0):
    if pad: X = np.pad(X, ((pad,pad),(pad,pad),(0,0)))
    H, W, _ = X.shape
    kh, kw, _, Co = K.shape
    out_h = (H - kh) // stride + 1; out_w = (W - kw) // stride + 1
    Y = np.zeros((out_h, out_w, Co))
    for i in range(out_h):
        for j in range(out_w):
            Y[i, j] = np.tensordot(X[i*stride:i*stride+kh, j*stride:j*stride+kw], K, axes=([0,1,2],[0,1,2]))
    return Y

def relu(x): return np.maximum(0, x)

# A residual block: y = relu(conv2(relu(conv1(x))) + x)
def residual_block(X, W1, W2, b1, b2):
    h = relu(conv2d(X, W1, pad=1) + b1)
    h = conv2d(h, W2, pad=1) + b2
    return relu(h + X)                           # shortcut

# Compare gradient norm through depth: plain vs residual
def plain_block(X, W1, W2, b1, b2):
    h = relu(conv2d(X, W1, pad=1) + b1)
    h = relu(conv2d(h, W2, pad=1) + b2)
    return h

C = 8
X = rng.normal(size=(16, 16, C))
def make_W(out_C=C): return rng.normal(scale=0.1, size=(3, 3, C, out_C))

for D in [1, 5, 10, 20]:
    h_plain = X.copy(); h_res = X.copy()
    Ws = [(make_W(), make_W(), np.zeros(C), np.zeros(C)) for _ in range(D)]
    for W1, W2, b1, b2 in Ws:
        h_plain = plain_block(h_plain, W1, W2, b1, b2)
        h_res   = residual_block(h_res, W1, W2, b1, b2)
    print(f"depth {D:>2}: plain output norm = {np.linalg.norm(h_plain):.3e}, "
          f"residual = {np.linalg.norm(h_res):.3e}")

Plain networks collapse their activation norm to near zero as depth grows because the small initialization compounds; residual networks preserve the norm via the shortcut.

Exercises

Exercise 1. VGG's design replaces one 5×55 \times 5 convolution with two stacked 3×33 \times 3 convolutions of the same receptive field. With CC input and CC output channels at every layer (and no bias), count the weights of each option and compute the fraction saved.

Solution

Step 1. A k×kk \times k convolution from CC input to CC output channels has k2C2k^2 C^2 weights. The single 5×55 \times 5 layer has 25C225 C^2.

Step 2. Two stacked 3×33 \times 3 layers each have 9C29 C^2 weights, so 2⋅9C2=18C22 \cdot 9 C^2 = 18 C^2 together.

Step 3. The ratio is 18C225C2=0.72\frac{18 C^2}{25 C^2} = 0.72, a 28%28\% reduction, and the stacked version adds an extra nonlinearity between the two layers. The receptive fields match: two 3×33 \times 3 layers cover 1+2⋅(3−1)=51 + 2 \cdot (3 - 1) = 5 positions per axis, the same as one 5×55 \times 5.

Exercise 2. Backpropagation through a residual block y=F(x)+xy = F(x) + x gives xˉ=yˉ+yˉ ∂F/∂x\bar x = \bar y + \bar y\,\partial F/\partial x. Explain why this keeps gradients from vanishing in a deep stack, and contrast it with a plain block y=F(x)y = F(x).

Solution

Step 1. For a plain block y=F(x)y = F(x), the upstream gradient is xˉ=yˉ ∂F/∂x\bar x = \bar y\,\partial F/\partial x. Composing LL such blocks multiplies LL Jacobians ∂F/∂x\partial F/\partial x; if each has spectral norm below 11, the product shrinks geometrically and the gradient reaching early layers vanishes.

Step 2. For a residual block, xˉ=yˉ(I+∂F/∂x)\bar x = \bar y(I + \partial F/\partial x). The identity term means the upstream gradient always contains a copy of yˉ\bar y passed through unscaled, regardless of how small or badly conditioned ∂F/∂x\partial F/\partial x is.

Step 3. Composing LL residual blocks expands into a sum of products over subsets of the ∂F/∂x\partial F/\partial x factors, and the all-identity term contributes yˉ\bar y undamped. The gradient to early layers cannot collapse to zero from the depth alone, which is why residual stacks train at depths plain stacks cannot.

Exercise 3 (code). A residual block with FF set to the zero function should compute the identity on nonnegative inputs (since y=relu(F(x)+x)=relu(x)y = \mathrm{relu}(F(x) + x) = \mathrm{relu}(x)). Verify this on a random nonnegative feature map.

Solution
python · numpy
import numpy as np

rng = np.random.default_rng(0)

def conv2d(X, K, stride=1, pad=0):
    if pad:
        X = np.pad(X, ((pad, pad), (pad, pad), (0, 0)))
    H, W, _ = X.shape
    kh, kw, _, Co = K.shape
    out_h = (H - kh) // stride + 1
    out_w = (W - kw) // stride + 1
    Y = np.zeros((out_h, out_w, Co))
    for i in range(out_h):
        for j in range(out_w):
            Y[i, j] = np.tensordot(
                X[i*stride:i*stride+kh, j*stride:j*stride+kw], K,
                axes=([0, 1, 2], [0, 1, 2]))
    return Y

def relu(x):
    return np.maximum(0, x)

def residual_block(X, W1, W2):
    h = relu(conv2d(X, W1, pad=1))
    h = conv2d(h, W2, pad=1)
    return relu(h + X)                            # shortcut

C = 4
X = np.abs(rng.normal(size=(8, 8, C)))            # nonnegative feature map
W_zero = np.zeros((3, 3, C, C))                   # F is the zero function

y = residual_block(X, W_zero, W_zero)
assert np.allclose(y, X)                          # block degenerates to identity
print("max abs deviation from identity:", np.abs(y - X).max())

With the residual branch zeroed, the block reproduces its nonnegative input exactly, the property that lets a deep ResNet fall back to the identity rather than degrade.

The next lesson surveys modern CNN families that build on this ResNet template.


References

  1. [1]LeCun, Y., Bottou, L., Bengio, Y., & Haffner, P. (1998). "Gradient-based learning applied to document recognition". Proceedings of the IEEE, 86(11), 2278-2324. doi:10.1109/5.726791. LeNet-5, the original convolution-pooling classifier.↩
  2. [2]Krizhevsky, A., Sutskever, I., & Hinton, G. E. (2012). "ImageNet classification with deep convolutional neural networks". NeurIPS. AlexNet: ReLUs, dropout, and GPU training on ImageNet.↩
  3. [3]Simonyan, K., & Zisserman, A. (2014). "Very deep convolutional networks for large-scale image recognition (VGG)". arXiv:1409.1556. Uniform stacks of 3x3 convolutions.↩
  4. [4]He, K., Zhang, X., Ren, S., & Sun, J. (2016). "Deep residual learning for image recognition (ResNet)". CVPR. arXiv:1512.03385. The residual block that made very deep networks trainable.↩