Convolutions, recurrences, and residual connections
Feed a 224 by 224 color photo to a fully connected layer with 1,000 hidden units and that one layer needs million weights. Worse, nothing in it knows that a cat in the top-left corner is the same thing as a cat in the center. It would have to learn "cat" separately at every position.
For a decade, two architectures solved problems like this by building the structure of the data into the network. Convolutional networks assume images are made of local patterns that can appear anywhere. Recurrent networks assume sequences are processed one step at a time with the same rule. Then residual connections and gates made both trainable at depth. Transformers have since replaced them for most tasks, but their ideas did not go away. Patch embeddings are convolutions, the residual stream is a residual connection, gating returns in SwiGLU and in state-space models, and the fixed-size-state bottleneck of recurrent networks is exactly what attention was invented to remove. This section teaches convolutions and residual connections at interview depth, including the forward and backward passes you may be asked to write, and gives recurrence a short treatment that Chapter 3 completes.
Convolution is a small dense layer, reused everywhere
Take a 1-D signal and a filter (kernel) . Slide the filter along the signal and, at each position, take the dot product with the three values under it. The first output is , and every other position also gives , so the output is . This filter measures the local slope, and the signal has a constant slope. (Deep learning libraries compute exactly this sliding dot product and call it convolution, although mathematicians call it cross-correlation. True convolution flips the filter first. Since the filter is learned, the distinction does not matter.)
For images with channels the idea is the same in more dimensions. An input ( channels, height , width ) and a bank of filters with biases give an output with channels,
where is the stride, and the input has been zero-padded beforehand if requested. In plain words, each output value is a small dense layer applied to one patch across all input channels, and the same weights are used at every position.
That one sentence contains two inductive biases. The first is locality. Each output depends only on a small neighborhood, so the network builds up from edges to textures to parts to objects. The second is weight sharing. The same filter is applied everywhere, which makes the layer translation equivariant. Shift the input by one pixel and the output shifts by one pixel (exactly so for stride 1), so a pattern learned in one place is recognized in every place. Weight sharing is also what makes convolutions cheap in parameters. A convolution from 64 to 128 channels has weights (plus 128 biases), however large the image is.
Its compute, though, does grow with the image. Every output position does multiply-adds per filter. On a feature map that same layer costs million multiply-adds. Convolutional networks are compute-heavy and parameter-light, the reverse of the fully connected layers they replaced.
The filter lights up on the left and right edges of the square, with opposite signs. Shift the square and the response shifts with it, unchanged. That is equivariance, and it is why a CNN does not need to see a cat at every position to recognize one anywhere.
Padding, stride, and the output-size formula
Three settings control the output's spatial size. Padding adds zeros around the border so filters can be centered on edge pixels. Stride moves the filter positions at a time, downsampling the output. Dilation spaces out the filter's taps, so a filter with covers a area with 9 weights. For an input of size along one axis and a kernel of size ,
In plain words, the dilated kernel spans inputs. Count how many times that span fits in the padded input, stepping by .
Two cases are worth memorizing. With stride 1 and for odd ("same" padding), the output has the input's size. And the ResNet stem works like this, from a image. A convolution with stride 2 and padding 3 gives . A max pool with stride 2 and padding 1 gives . The first ResNet stage keeps , and each of the last three halves it, to 28, 14, and finally 7.
Receptive fields
The receptive field of a unit is the region of the input it can see. One layer sees a patch. A unit two layers up sees a patch of units, each seeing pixels, overlapping, so pixels. Three layers give .
The general rule tracks two numbers per layer. The jump is the distance in input pixels between adjacent units of layer , and the receptive field is the span of input pixels one unit sees. Starting from the input with and ,
In plain words, each layer widens the view by its extra kernel taps, measured in the input-pixel spacing of the layer below, and strides multiply that spacing for every layer that follows.
So strides make receptive fields grow fast. Through the ResNet stem above, the stride-2 convolution gives , . The stride-2 pool adds , so , . One more convolution adds , so . Explore it below. Click any output unit to light up everything it depends on.
This arithmetic is behind a classic design argument (Simonyan & Zisserman, 2015, the VGG paper). Three stacked layers have the same receptive field as one layer, but with weights instead of , and with two extra nonlinearities in between. One caution. The receptive field is where a unit can look, not where it does. Luo et al. (2016) showed that the effective receptive field, measured by how much each input pixel actually influences the output, is roughly Gaussian and occupies only a fraction of the theoretical one.
Two more designs save weights in modern CNNs. A convolution looks at a single pixel, so it is a linear layer applied at every position, mixing channels without mixing space. A depthwise separable convolution (popularized by Xception, Chollet, 2017, and MobileNets, Howard et al., 2017) splits a convolution from to channels into a per-channel filter ( weights) followed by a convolution that mixes channels ( weights), instead of weights in one go. It keeps a fraction of the original weights, so for and it needs about 8.7 times fewer weights and multiply-adds. Mobile vision networks are built from it.
Output sizes and receptive fields
Implement conv_out_size(n, k, s=1, p=0, d=1) from the formula above, and receptive_field(layers) where layers is a list of (k, s, d) tuples from input to output, returning (r, j), the receptive field and jump of the last layer.
Pooling
A pooling layer summarizes each window with a fixed function, usually the maximum (max pooling) or the mean (average pooling), with no weights. A max pool with stride 2 halves each spatial size and keeps the strongest response in each window, which adds a little invariance to small shifts. Global average pooling averages each channel over the whole map, turning a feature map into a -dimensional vector. That is how a ResNet produces its final image embedding, a 2,048-dimensional vector for ResNet-50, which §1.8 uses as an image encoder. Many modern architectures replace pooling with strided convolutions, which learn how to downsample.
The backward pass of max pooling is worth knowing, since interviewers ask it. The output is the maximum of its window, so its derivative is 1 with respect to the input that achieved the maximum and 0 for the others. The upstream gradient is routed to the argmax position of each window, and if windows overlap, an input that is the maximum of several windows accumulates gradient from all of them.
Max pooling, forward and backward
Implement maxpool_forward(X, k, s) for X of shape (N, C, H, W) with a k x k window and stride s (no padding), returning (Y, cache), and maxpool_backward(dY, cache) returning dX. Each window's gradient goes to the position of its maximum (the first one, if there is a tie). Overlapping windows (s < k) must accumulate.
Convolution as one big matrix multiply
The six nested loops of the convolution formula are hopeless in Python and slow even in C. Every fast convolution reorganizes the work into something faster, and the oldest and simplest reorganization is im2col ("image to columns"). Copy every patch of the input into a row of a big matrix, and reshape the filters into a matrix with one column per filter. For a batch of images and an output of positions per image, the whole convolution is then one matrix multiply,
In plain words, unroll the patches into rows, and the convolution becomes the dense layer it secretly was all along, applied to every patch at once.
This hands the work to a matrix-multiply kernel (GEMM), the most optimized routine on any hardware, which is why early GPU frameworks used it. The price is memory, since every input value is copied into up to patches. For a convolution the column matrix is about 9 times larger than the input. Modern libraries such as cuDNN choose among several algorithms (implicit GEMM, which forms the patches on the fly, FFT-based methods, and Winograd transforms) per layer. Notice also how ViT's patch embedding fits in. It is a convolution whose kernel size equals its stride, so the patches do not overlap and im2col is a single reshape and transpose, the patchify step that starts every Vision Transformer (§1.8 builds it).
Conv2d with stride and padding, via im2col
Implement conv2d(X, W, b, stride=1, pad=0) for input X of shape (N, C, H, W_in), filters W of shape (F, C, kh, kw) (kernels need not be square), and bias b of shape (F,), returning (N, F, H_out, W_out). Use im2col and a single matrix multiply. No Python loops over the batch, the channels, or the output positions.
The backward pass scatters columns back
Training needs the gradients of a convolution too, and im2col makes them almost free to derive, because the forward pass is now an ordinary linear layer, . Reshape the upstream gradient into the same layout and call it . Then the linear-layer rules of §0.2 give
In plain words, each filter's gradient correlates its output errors with the patches that produced them, and each patch's gradient is the errors sent back through the filters.
The only new step is getting from the gradient of the patch matrix back to the gradient of the image. Each pixel was copied into several patches, so by the accumulation rule of §0.3 its gradient is the sum of the gradients of all its copies. That inverse of im2col, adding every patch entry back to the pixel it came from, is called col2im. It has a neat consequence. For stride 1, the gradient with respect to the input is itself a convolution of the upstream gradient with the filters flipped and their input and output channels swapped. So computing the input gradient costs about as much as the forward pass, the weight gradient costs about as much again (the same factor of two as for a linear layer in §0.2), and the transposed convolution used for upsampling in generative models is exactly this input-gradient operation, run forward.
Conv2d backward with col2im
Implement conv2d_backward(dY, X, W, stride=1, pad=0) returning (dX, dW, db) for the convolution of the previous exercise, where dY has shape (N, F, H_out, W_out). No Python loops over the batch, the channels, the filters, or the output positions. A loop over the kh * kw kernel offsets is allowed, and is the simplest way to do col2im. The setup provides conv2d_ref, a slow loop version of the forward pass, which the tests use.
Residual connections, and why ResNets train
By 2015, networks with about 20 layers worked, but making them deeper made them worse, and not from overfitting. He et al. (2016) showed that a 56-layer plain network had higher training error than a 20-layer one. That is strange, since the deeper network could in principle copy the shallow one and set its extra layers to the identity. The optimizer could not find that solution. They called this the degradation problem.
Their fix was to make the identity the default. A residual block computes
where is a small stack of layers (two or three convolutions with normalization and ReLU in ResNet). In plain words, each block adds a correction to its input instead of replacing it. If the best thing a block can do is nothing, it only has to push its weights toward zero, which is easy.
The gradient tells the deeper story. Unroll the recursion from block to the last block , so . Then by the chain rule
In plain words, the gradient at any block contains the gradient at the top unchanged, plus whatever flows through the residual branches. In a plain network the gradient must pass through every layer's Jacobian, a product that can vanish. Here there is a direct path from the loss to every layer, which cannot vanish unless the branch terms cancel it exactly (He et al., 2016b, analyze this). Veit et al. (2016) offer a complementary picture. Unrolled, a residual network is a sum over exponentially many paths of different lengths, and most of the gradient flows through the short ones, so it behaves like an ensemble of relatively shallow networks.
There is one catch, and the cell shows it. Adding branch outputs to the stream also adds their variance, so in an unnormalized residual network the activations, and the gradients, grow with depth. ResNets control this with BatchNorm inside each branch, often initializing the last normalization gain of each branch to zero so every block starts as the identity (Goyal et al., 2017). Transformers control it with LayerNorm and by scaling down the branches at initialization, the rule that §5.7 derives.
With PyTorch's default initialization, the 50-layer plain network's gradient vanishes by a factor of about , as §0.7 predicts for a per-layer gain below 1. The residual network keeps a gradient at the first layer, but its unnormalized stream grows to enormous scale. Scaling each branch by keeps the activations near 1 and the gradient within a small factor of it (here 3.8). That combination, an identity path plus controlled branches, is what makes networks of hundreds of layers trainable, and it is the backbone of every Transformer (the residual stream, §5.1).
Recurrence, in brief
Images come on a grid. Text, audio, and time series come in order, and with any length. A recurrent neural network (RNN) reads such a sequence one step at a time, carrying a hidden state that summarizes everything seen so far. At each step, with input ,
with , , and . In plain words, mix the old state with the new input, squash, and repeat with the same weights at every step. That is weight sharing again, across time instead of space. Training unrolls the recurrence into a deep graph with one copy of the cell per step, all sharing weights, and backpropagates through it (backpropagation through time). The shared weights' gradient is the sum of the contributions from every step, the accumulation rule of §0.3.
The trouble is depth in time. The gradient from step back to step is a product of Jacobians, (with the factor leftmost), all involving the same matrix. Its norm is at most , where is the spectral norm, the largest factor by which a matrix can stretch a vector, equal to its largest singular value (§0.10), since the tanh derivatives are at most 1. So when the largest singular value of is below 1 the signal from distant steps vanishes exponentially. A largest singular value above 1 is necessary for the gradient to explode, though not sufficient, because the tanh derivatives can still shrink it (Pascanu et al., 2013). A feed-forward network can fix this layer by layer with initialization and normalization. An RNN cannot, because every step reuses one matrix. Clipping (§0.6) handles explosions. Vanishing needs a change of architecture.
That change is gating. The LSTM (Hochreiter & Schmidhuber, 1997, with the forget gate added by Gers et al., 2000) keeps a separate cell state updated additively, , where is elementwise multiplication, and are learned gates between 0 and 1, and is a candidate update. Along that path the Jacobian contains the direct term instead of a product with a weight matrix, so with the forget gate open, gradients cross many steps nearly unchanged. It is the residual connection of the previous section, with a learned valve on the identity path. The GRU (Cho et al., 2014) achieves the same with fewer gates. The full story, RNN language models, backpropagation through time implemented and checked, the eigenvalue analysis, and LSTM and GRU cells built from scratch, is in recurrent language models (§3.3).
Why Transformers replaced them, and what survived
| CNN | RNN, LSTM, GRU | Transformer | |
|---|---|---|---|
| built-in assumption | locality and translation equivariance | sequential processing, same rule each step | none beyond permutation symmetry plus positions |
| path length between distant positions | grows with distance over receptive field size | grows with distance, steps | one step, |
| parallel over positions in training | yes | no, step waits for step | yes |
| cost per layer in sequence length | linear | linear | quadratic (attention) |
| memory at inference | a fixed window of past activations | a fixed-size state | grows with context (the KV cache) |
The decisive rows are the middle two. An LSTM must squeeze everything about the past through one fixed-size state and carry it through sequential steps, which is slow on parallel hardware and still lossy over long ranges. Attention lets every position look at every other directly, in parallel (§3.6 tells this story, and §4.7 its costs). CNNs ceded ground on images for a subtler reason. Their built-in biases help with little data and limit them with a lot, so with hundreds of millions of training images a Transformer that learns its own biases wins.
But the ideas survived. Residual connections are in every Transformer block. Gates live on in SwiGLU feed-forward layers. Convolutions still make patch embeddings and audio front ends. And the RNN's fixed-size state, with its cheap constant-memory inference, has returned in state-space models such as Mamba, which use linear recurrences that can be trained in parallel, because a linear recurrence, unlike a tanh one, can be computed with a parallel scan.
In PyTorch
Read more
- Dumoulin & Visin (2016). A guide to convolution arithmetic for deep learning. Every case of padding, stride, and dilation, with diagrams.
- Araujo, Norris & Sim (2019). Computing Receptive Fields of Convolutional Neural Networks. The receptive-field recurrences derived carefully, with interactive figures.
- He, Zhang, Ren & Sun (2016). Deep Residual Learning for Image Recognition. The degradation problem and the residual fix. Their follow-up, Identity Mappings in Deep Residual Networks, analyzes the gradient path.
- Veit, Wilber & Belongie (2016). Residual Networks Behave Like Ensembles of Relatively Shallow Networks. The many-paths view of ResNets.
- Pascanu, Mikolov & Bengio (2013). On the difficulty of training recurrent neural networks. Vanishing and exploding gradients in RNNs, and clipping.
- Luo, Li, Urtasun & Zemel (2016). Understanding the Effective Receptive Field in Deep Convolutional Neural Networks. Why the effective receptive field is much smaller than the theoretical one.