Convolutional networks: filters, arithmetic, im2col and ResNet
Before this lesson: lesson 03 (backpropagation; the linear-layer rule ) and lesson 05 (BatchNorm and residual connections). Helpful for the folded Fourier box: linear-algebra lesson 19 (circulant matrices and the DFT). Time: about 85 minutes, plus the notebook.
Why this matters #
A convolution layer builds one assumption into the network: a useful pattern is local, and it is useful at every position. This cuts the number of parameters by orders of magnitude and lets CNNs learn from less data on grids (images, audio, weather fields). ResNet, one of the most cited papers in science, is a stack of convolution blocks. In generative modelling, the autoencoder and the U-Net denoiser of latent diffusion models (Stable Diffusion) are convolutional ResNet blocks. In ML for science, a convolution with a fixed kernel is a finite-difference stencil, and U-Nets are a standard baseline for fields on grids. In interviews you are asked to compute shapes, parameter counts and receptive fields by hand, and sometimes to write the backward pass in NumPy.
1. Predict first #
2. The picture #
One dimension first #
Take a signal with one channel and a kernel of size , . The layer computes
Each output is the dot product of with a window of three neighbouring inputs. The window slides by one position for each output.
Deep-learning libraries call this "convolution", but it is a cross-correlation: the kernel is not flipped. A true convolution flips the kernel: . Because is learned, the flip does not matter for what a layer can represent. It does matter in the backward pass, as we will see.
As a matrix, with a banded matrix. For :
A matrix whose diagonals are constant is a Toeplitz matrix. Two ideas are visible in :
- Locality: each row has only nonzeros. Each output sees a small neighbourhood.
- Weight sharing: every row holds the same three numbers, shifted by one place. A dense layer has parameters; this layer has , whatever the length .
Because every row is a shifted copy of the first row, shifting the input shifts the output. This is translation equivariance; section 3 makes it precise.
Filters on an image #
In 2-D the kernel is a small grid, for example . At each position the output is the sum of the kernel weights times the pixels under the window. The output image is a feature map: it is large where the window looks like the kernel.
Move the yellow window over either image (hover, tap, or focus the figure and use the arrow keys). The panel shows the dot product of the kernel with the pixels under the window. Then pick another kernel or edit one entry. Notice: edge-x answers only on vertical edges, edge-y only on horizontal edges, and both are 0 where the image is constant. Output colours: blue = positive, magenta = negative. The image is padded with zeros, so the feature map has the same size, .
Read the edge-x kernel (the Sobel filter) as "right column minus left column". Where the brightness grows from left to right, the output is positive; where it falls, negative; where it is constant, the weights sum to 0 and the output is 0. A CNN does not get these kernels from us: it learns its kernels by gradient descent. The first layer of a trained image network learns edge and colour detectors that look much like these.
Many channels in, many channels out #
A colour image has channels (red, green, blue), and hidden layers have tens to hundreds. One output channel uses one kernel of shape : it looks at all input channels at once and adds everything up. A layer with output channels has such kernels, so the weights have shape (PyTorch order).
For one input image of shape , already zero-padded, with stride (the window moves pixels per output):
Here is the output channel, the output position, the input channel and the position inside the kernel. The sum runs over all input channels and all kernel positions , . ( is the weight tensor; with a subscript is the input width.) Every output value is a dot product between one flattened kernel (length ) and one flattened patch of the input. Keep this sentence in mind: it is the whole idea of im2col in section 3.
You apply the edge-x kernel at a position where all nine pixels under the window equal . What is the output?
The output is , and the weights sum to . An edge filter answers to changes of brightness, not to brightness. The position does not matter: the same weights are used everywhere (weight sharing).
3. Definitions and main results #
Arithmetic: output size, parameters, cost #
Notation along one spatial axis: input length , kernel size , stride , zero padding on each side, and dilation (the distance between neighbouring kernel taps; is an ordinary kernel, skips every other pixel).
A dilated kernel covers pixels. The padded input has pixels. The first window starts at 0, and each next window starts pixels later. The last window must end inside the padded input. Counting the windows gives the output size:
This is the formula of Dumoulin and Visin (Relationship 6, and Relationship 15 with dilation), and PyTorch uses it with the same floor. The floor means that pixels at the end that do not fill a whole window are dropped. "Same" padding: for odd , , , the choice gives .
Change one slider at a time and predict the output size before you look. The violet squares are the kernel taps of the current output position (yellow), dashed squares are zero padding. Press Play or Next position to slide the window. Try the preset floor drops a pixel: input pixels that no window ever touches turn magenta. Try dilation 2: the same 9 taps cover a area.
Parameters. A layer has weights and biases:
The count does not depend on or : the weights are shared over all positions. With groups (the channels are split into groups that do not mix), each output channel sees only input channels: weights. A depthwise convolution has : one kernel per channel.
Cost. A multiply-accumulate (MAC) is one multiplication and one addition. Each weight is used once at each output position, so one image costs
(divide by with groups). FLOPs MACs. Many papers, ResNet included, write "FLOPs" when they mean multiply-adds, so always say which one you mean. The parameters do not grow with the image size, but the cost does: it grows with the number of output positions.
Receptive field #
The receptive field of a unit is the set of input pixels that can change its value. Along one axis its size grows layer by layer. Let be the jump of layer : the distance, in input pixels, between two neighbouring units of layer . Strides multiply the jump. With and for the input:
So , the product of all earlier strides.
Why this is true.
- A unit of layer reads units of layer .
- Neighbouring taps are units apart in layer , which is input pixels.
- So the first and the last of these units are input pixels apart.
- Each of them sees input pixels. The union spans pixels.
Pooling layers count like convolutions (a pool with stride 2 has , ). This is the theoretical receptive field. The effective receptive field, where the gradient is large, is smaller and roughly Gaussian (Luo et al., 2016): pixels near the centre have many more paths to the unit than pixels at the edge.
Click a unit in any row (the input is at the bottom). The units it depends on light up in blue, layer by layer, and the yellow bracket is its receptive field. The panel shows the recursion. Compare three 3-convs with one 7-conv: the same . With dilation 1, 2, 4 the field grows fast; edit the table to use dilation 3 in one layer and look for holes.
The ResNet stem is Conv2d(3, 64, kernel_size=7, stride=2, padding=3) on a image. What is the output shape, and how many weights (without biases) does the layer have?
. Weights: . The answer forgets ; forgets the padding; multiplying by counts MACs, not weights.
Three convolutions with stride 1 are followed by a max pool with stride 2. What is the receptive field of one unit after the pool (along one axis)?
After the three convolutions and the jump is still . The pool has , so it adds : . Its stride does not enter its own term; it doubles the jump for the next layers (a conv after it would add ).
im2col: a convolution is one matrix product #
Every output value is (flattened kernel) (flattened patch). So collect all patches as the columns of one matrix and all kernels as the rows of another.
- im2col ("image to columns") builds of shape
: one column per output position,
one row per kernel tap. Row and column hold
. This is the layout of PyTorch's
F.unfold. - Reshape to of shape : one row per output channel.
Then
has shape , and reshaping it gives . Transpose both sides and you get the row form of this track (lesson 01): . This is a linear layer applied to "examples", one per patch, all with the same weights. That is weight sharing, written as a matrix product.
Step through the 9 patches. Each patch of the 4 × 4 input is copied into one column of cols; the kernel becomes one row; the product is the output, read row by row. Then switch to backward: the gradient of the columns is , and col2im adds each column back onto its patch. Watch the centre pixels of collect four contributions.
Memory cost. cols holds numbers, about times the size of the input. For a "same" convolution that is : a fp32 input (0.8 MB) becomes 7.2 MB of columns, and 1.85 GB for a batch of 256. This is one reason GPU libraries such as cuDNN use implicit GEMM (the patches are built tile by tile and never stored), Winograd or FFT algorithms instead.
The backward pass #
Let be the upstream gradient (same shape as ), as in lesson 03. We want , and .
Theorem (backward pass of a convolution). Take stride 1, no padding, one image: . Then
where outside its index range.
In matrix form: , , and . is a "full" cross-correlation of (zero-padded by on each side) with the kernel rotated by and with the two channel axes swapped: a transposed convolution.
Why this is true (intuition). In the forward pass, pixel is used by every window that covers it, each time with a different weight. So its gradient collects one term from each of those windows. Think of each as a "stamp": it puts a copy of the kernel, scaled by , onto the input grid at position . Overlapping stamps add. The index (instead of ) is the flip: forward is a cross-correlation, so backward is a true convolution.
Proof of the theorem (six steps)
- enters every output of channel with coefficient 1, so is the sum of .
- multiplies in output and nowhere else. Summing over all outputs gives : the cross-correlation of the input with the output gradient.
- Pixel enters output exactly when and , with weight . So .
- Substitute , and pad with rows and columns of zeros on each side (call it ). Then , and with :
This is a cross-correlation of with the rotated, channel-swapped kernel. Its output size is , as it must be. 5. In matrix form, is a linear layer, so (lesson 03) , and . 6. is a linear map of . The vector-Jacobian product of a linear map is its adjoint (linear-algebra lesson 15): . is col2im: it sends every entry of a column back to the pixel it was copied from and adds the contributions, because a pixel lies in up to patches. So .
The map is linear in , with some matrix (like the Toeplitz matrix of section 2).
The theorem says . That is why the operator is called the transposed
convolution. With stride , first insert zeros between the entries of , then do
the same full correlation. The transposed convolution is not an inverse, and col2im is not an
inverse of im2col either: multiplies each pixel by the number of
patches that contain it. U-Net decoders use the same operator, with learned weights, to upsample
(ConvTranspose2d).
Translation equivariance, and where it breaks #
Let shift a signal by positions: . A layer is equivariant to shifts if : shifting the input shifts the output by the same amount. It is invariant if .
Stride 1 is equivariant on a circular (or infinite) domain. Proof in 1-D, indices taken mod :
It breaks in three ways.
- Zero padding. Near the border the shifted input meets zeros instead of the pixels that "wrapped around", so equivariance holds only away from the border. Padding also lets a CNN learn absolute position: a unit can tell that it is near the border.
- Stride . A strided layer is the stride-1 layer followed by keeping every -th output: . Then . If , this is : the output shifts by . If is not a multiple of , the layer now keeps the other samples of (for : the odd ones instead of the even ones), and in general no shift of the old output matches. This is aliasing.
- Pooling. Max pooling gives some invariance to small shifts inside a window, but it also subsamples, so it has the same stride problem. A CNN becomes (approximately) invariant only through global pooling at the end.
Move the shift slider and switch the stride. Row 2 is the stride-1 output of the shifted input; the yellow rings mark the samples that stride keeps. Row 3 compares the strided output of the shifted input (blue) with the best shift of the strided output of the original input (aqua rings; magenta where they disagree). With , even shifts match exactly and odd shifts do not.
This answers question 3 of section 1: no. With stride 2, a one-pixel shift gives a different output, not a shifted one.
A convolution with stride 2 uses circular padding, so there are no border effects. You shift the input by 3 pixels. What happens to the output?
, and is always odd, while the old output used (even). Only shifts by a multiple of the stride give . Convolutions are equivariant (at stride 1), not invariant.
Convolution, circulant matrices and the Fourier transform
On a circular domain of length , the 1-D layer is with (where for ). The entries of depend only on (mod ): is a circulant matrix. By linear-algebra lesson 19 (Theorem 2), a linear map commutes with the cyclic shift exactly when it is circulant. So the linear, shift-equivariant maps on a periodic signal are exactly the circular convolutions. Equivariance is not an accident of convolutions; it characterizes them.
By Theorem 1 of the same lesson, every circulant is diagonalized by the DFT: . So a circular convolution is a product in Fourier space, and it costs with the FFT instead of . That is the basis of FFT convolution for large kernels, and of the Fourier neural operator (FNO), which learns directly for the lowest frequencies: a global kernel instead of a local one. In 2-D, a periodic convolution is a block-circulant matrix with circulant blocks, and the 2-D DFT diagonalizes it.
Pooling, convolutions and global average pooling #
- Max pooling ( window, stride ) takes the maximum of each window, channel by channel. It has no parameters. Its backward pass sends each output gradient to the argmax of its window (the position of the maximum) and 0 to the other positions; with overlapping windows the contributions add. Average pooling takes the mean, so its gradient is spread evenly: to each pixel of the window.
- Global average pooling averages each channel over all positions: . The classifier head after it then works for any input size.
- A convolution applies the same linear map
at every pixel. It is a
Linearlayer over the channel axis, shared over positions. It changes the number of channels and mixes them cheaply ( weights).
ResNet blocks #
ResNet (He et al., 2016) is a stack of residual blocks (lesson 05 explains why the identity path makes deep networks trainable).
- The basic block (ResNet-18 and ResNet-34): conv → BN → ReLU → conv → BN, plus the identity, then ReLU.
- The bottleneck block (ResNet-50, -101, -152): () → () → (), each with BN, ReLU between them, plus the identity, then ReLU.
The bottleneck runs the expensive convolution at a quarter of the width. Its weights: . A basic block at 256 channels has , about more. The point of the paper: a bottleneck costs about as much as a basic block at 64 channels ( weights), so the network can be deeper and keep a wide (256-channel) residual stream at about the same cost.
When the shape changes (stride 2 and twice the channels), the shortcut is a strided convolution, a projection shortcut. The original (v1) models put the stride in the first convolution of the branch; torchvision's "v1.5" puts it in the convolution. Pre-activation ResNets (He et al., 2016b) reorder each branch to BN → ReLU → conv, so the identity path carries no nonlinearity at all. This is the same idea as pre-LN transformers (lesson 05).
U-Net (Ronneberger et al., 2015) has an encoder that halves the resolution step by step (strided convolutions or pooling) and doubles the channels, and a decoder that doubles the resolution again with transposed convolutions (or upsampling followed by a convolution). Skip connections copy each encoder feature map to the decoder stage of the same resolution, where it is concatenated. So the decoder gets both the coarse context and the fine detail. Diffusion models and many ML-for-science models (weather, segmentation of microscopy images) use U-Nets.
Answers to section 1. (1) weights (plus 64 biases), on any map size, because the weights are shared over positions. Only the cost grows: million MACs, and four times that on . (2) A transposed convolution: a full cross-correlation of with the -rotated, channel-swapped kernel, equal to .
4. Worked example #
(a) Shapes, parameters, MACs and receptive field #
Input ; all convolutions are with bias.
| Layer | Output | Parameters | MACs | , |
|---|---|---|---|---|
| conv , , | 3, 1 | |||
| conv , , | 5, 2 | |||
| max pool , | 0 | — | 7, 4 | |
| conv , , | 15, 4 |
Step by step:
- First conv: . , .
- Second conv: . Each kernel has weights, so MACs . , .
- Pool: . , .
- Last conv: . .
The second and the last conv have the same MACs. The last one has 4 times fewer output positions ( instead of ), but twice as many input channels and twice as many output channels.
(b) im2col and the backward pass by hand #
One channel, stride 1, no padding, :
The output is ().
Forward. cols has one row per kernel tap in the order , and one column per output position in the order . Column 1 is the top-left patch read row by row: . All four:
, column by column: ; ; ; . So .
Backward. Take , so .
- = (column 1 of cols) (column 4 of cols) . So . Check with the formula: ; for example ✓.
- by stamping: stamps at the top left, and stamps at the bottom right:
The centre pixel is in both patches, so it gets .
- The same result comes from the full correlation of (padded by one zero on each side) with . For example : the window at the top left of the padded is , and its dot product with is ✓.
In example (b), keep and but take . What is ?
, because only is nonzero. That is the patch of output position : , column 2 of cols. The first option is the patch of position ; the third is itself, which is what (not ) is built from; the last is that patch rotated by .
5. In code #
PyTorch checks the hand computation of example (b). F.unfold is im2col and F.fold is col2im.
import torch
import torch.nn.functional as F
x = torch.tensor([[1., 2, 0], [0, 1, 3], [2, 1, 1]]).reshape(1, 1, 3, 3).requires_grad_()
w = torch.tensor([[1., 0], [-1, 2]]).reshape(1, 1, 2, 2) # (C_out, C_in, kh, kw)
y = F.conv2d(x, w) # cross-correlation, stride 1, no padding
print(y.reshape(2, 2).tolist()) # [[3.0, 7.0], [0.0, 2.0]]
cols = F.unfold(x, kernel_size=2) # im2col: (N, C_in*kh*kw, H_out*W_out) = (1, 4, 4)
w_mat = w.reshape(1, -1) # (C_out, C_in*kh*kw)
print(torch.equal(w_mat @ cols, y.reshape(1, 1, 4))) # True: conv = one matrix product
dy = torch.tensor([[1., 0], [0, -1]]).reshape(1, 1, 2, 2)
y.backward(dy) # autograd puts X̄ into x.grad
dw = (dy.reshape(1, 4) @ cols[0].T).reshape(2, 2) # W̄ = Ȳ cols^T
dcols = w_mat.T @ dy.reshape(1, 1, 4) # W^T Ȳ
dx_col2im = F.fold(dcols, (3, 3), kernel_size=2) # col2im
dx_transposed = F.conv_transpose2d(dy, w) # transposed convolution
print(dw.tolist())
# [[0.0, -1.0], [-1.0, 0.0]]
print(x.grad.reshape(3, 3).tolist())
# [[1.0, 0.0, 0.0], [-1.0, 1.0, 0.0], [0.0, 1.0, -2.0]]
print(torch.equal(x.grad, dx_col2im), torch.equal(x.grad, dx_transposed))
# True True
The arithmetic of example (a), the receptive field as the support of a gradient, the bottleneck count and the stride claim:
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(0)
layers = [nn.Conv2d(3, 16, 3, padding=1), nn.Conv2d(16, 32, 3, stride=2, padding=1),
nn.MaxPool2d(2, 2), nn.Conv2d(32, 64, 3, padding=1)]
h = torch.zeros(1, 3, 32, 32)
for layer in layers:
c_in = h.shape[1]
h = layer(h)
params = sum(p.numel() for p in layer.parameters())
macs = h[0].numel() * c_in * 9 if isinstance(layer, nn.Conv2d) else 0
print(type(layer).__name__, tuple(h.shape[1:]), params, macs)
# Conv2d (16, 32, 32) 448 442368
# Conv2d (32, 16, 16) 4640 1179648
# MaxPool2d (32, 8, 8) 0 0
# Conv2d (64, 8, 8) 18496 1179648
# Receptive field = the pixels where the gradient of one output unit is nonzero.
# AvgPool has the same window as MaxPool, but its gradient reaches every pixel
# of the window (MaxPool's gradient reaches only the argmax).
probe = nn.Sequential(nn.Conv2d(1, 1, 3, padding=1), nn.Conv2d(1, 1, 3, stride=2, padding=1),
nn.AvgPool2d(2, 2), nn.Conv2d(1, 1, 3, padding=1))
for m in probe:
if isinstance(m, nn.Conv2d):
nn.init.constant_(m.weight, 1.0)
x = torch.zeros(1, 1, 64, 64, requires_grad=True)
probe(x)[0, 0, 8, 8].backward()
rows = (x.grad[0, 0].abs().sum(dim=1) > 0).nonzero()
print("receptive field:", int(rows.max() - rows.min() + 1)) # receptive field: 15
count = lambda m: sum(p.numel() for p in m.parameters())
bottleneck = nn.Sequential(nn.Conv2d(256, 64, 1, bias=False),
nn.Conv2d(64, 64, 3, padding=1, bias=False),
nn.Conv2d(64, 256, 1, bias=False))
basic = nn.Sequential(nn.Conv2d(256, 256, 3, padding=1, bias=False),
nn.Conv2d(256, 256, 3, padding=1, bias=False))
print(count(bottleneck), count(basic)) # 69632 1179648
# Shift equivariance on a circular domain:
# exact at stride 1; at stride 2 only for even shifts.
x, w = torch.randn(1, 1, 16, 16), torch.randn(1, 1, 3, 3)
conv = lambda t, s: F.conv2d(F.pad(t, (1, 1, 1, 1), mode="circular"), w, stride=s)
shift = lambda t, k: torch.roll(t, k, dims=-1)
print(torch.allclose(conv(shift(x, 1), 1), shift(conv(x, 1), 1), atol=1e-6)) # True
print(torch.allclose(conv(shift(x, 2), 2), shift(conv(x, 2), 1), atol=1e-6)) # True
err = min(float((conv(shift(x, 1), 2) - shift(conv(x, 2), k)).abs().max()) for k in range(8))
print(err > 0.1) # True: no shift of the output matches
6. Where it shows up #
- Architectures. ResNet; U-Net; ConvNeXt (depthwise convolutions plus "MLP" layers); MobileNet (depthwise separable convolutions); the autoencoder of latent diffusion (Stable Diffusion downsamples by 8 in each direction). The patch embedding of a vision transformer is a convolution with kernel size = stride = patch size (lesson 11).
- Sequences. 1-D convolutions over time: WaveNet (dilated causal convolutions, so the receptive field grows exponentially with depth) and the local convolution inside Mamba blocks. Linear time-invariant state-space models such as S4 (lesson 16) can be computed as one very long convolution.
- ML for science. A convolution with a fixed kernel is a finite-difference stencil: the 5-point
Laplacian is . Because PyTorch
correlates, the kernel in
F.conv1dgives (a true convolution would give the minus sign). On a global latitude–longitude grid, pad circularly in longitude only,F.pad(x, (1, 1, 0, 0), mode="circular"), so the network does not see a false border at the date line. PhaseNet (seismic phase picking) is a 1-D U-Net on three-component waveforms. The Fourier neural operator replaces the local kernel by a global one in Fourier space (see the folded box in section 3).
7. Common mistakes #
- Dropping the floor. A length-7 input with , gives outputs; the last pixel is never used.
- Forgetting in the parameter count, or thinking that the count grows with the image size. It is , whatever , .
- Mixing MACs and FLOPs. They differ by a factor of 2.
- Receptive field without the jump. Each layer adds times the product of the earlier strides, not just .
- "CNNs are translation invariant." Convolution layers are equivariant, and exactly so only at stride 1 and away from the borders. Strides alias. Invariance comes, approximately, from global pooling at the end.
- Assignment instead of addition in col2im or in the max-pool backward pass. Overlapping patches
must add (
np.add.at, notx[idx] = ...). - Treating the transposed convolution as an inverse. It is the adjoint , not .
8. Check yourself #
- A 1-D input of length 100 goes through a conv (, , ), then a conv (, ,
). What are the output length and the receptive field?
Answer
, then . Receptive field : the stride of the second layer does not enter its own term. - Count the weights (no biases) of a depthwise separable convolution ( depthwise on 128
channels, then from 128 to 256) and of a standard convolution from 128 to 256.
Answer
against : about fewer. - By how much does im2col inflate the input for a "same" convolution at stride 1? At stride 2?
Answer
About : at stride 1, and about at stride 2. - Why does need col2im and not an inverse of im2col?
Answer
The backward pass of a linear map is its adjoint, not its inverse: . = col2im sends each column entry back to its pixel and adds the overlaps. A column matrix whose overlapping entries disagree is not im2col of any image, so there is no inverse to apply. - Why do three stacked convolutions (as in VGG) beat one convolution with the
same channels in and out?
Answer
Same receptive field (7), but weights instead of , and two extra nonlinearities in between. - A max pool (stride 2) sees the window , and the
upstream gradient of its output is . What gradient does each of the four pixels get? And with
average pooling?
Answer
Max pool: ; all of it goes to the argmax (the 5). Average pool: to each pixel.
What is the output size of a convolution with input , kernel , stride , padding , dilation ?
.
How many parameters does
Conv2d(C_in, C_out, k)have, and does the count depend on the image size?with bias. No: the weights are shared over all positions.
Give the receptive-field recursion.
, with jump and .
Compute the receptive field of three stacked convolutions with stride 1.
, the same as one convolution.
What does a deep-learning "convolution" compute, compared with a true convolution?
A cross-correlation: . A true convolution flips the kernel: .
What are the shapes in im2col for one image?
cols is , one column per patch; is ; .
What is the gradient of a convolution with respect to its input?
A transposed convolution: , a full correlation of with the -rotated, channel-swapped kernel.
Why does stride 2 break shift equivariance for a one-pixel shift?
The strided layer keeps every second sample of the stride-1 output. A one-pixel shift makes it keep the other samples, so the output is not a shift of the old one. Shifts by multiples of still work.
What does a convolution compute?
The same linear map at every pixel: a linear layer over the channel axis.
Why does ResNet-50 use the bottleneck block ( 256→64, 64→64, 64→256)?
The conv runs at a quarter of the width: weights instead of for a basic block at 256 channels, so the network can be deeper at the same cost.
9. Interview angle #
- "Output shape and parameters of this layer?" — Use the floor formula and ; say that the count is independent of and .
- "Write conv2d forward and backward in NumPy." — im2col plus one matrix product;
, with
np.add.at. - "Why do transposed convolutions give checkerboard artifacts?" — The stamped kernels overlap unevenly when the kernel size is not divisible by the stride (Odena et al., 2016). A common fix is resize, then convolve.
- "Why the bottleneck block?" — The convolution runs at a quarter of the width: about fewer weights than a basic block at the same outer width.
- "Is a CNN shift invariant?" — No. Its layers are equivariant at stride 1 (away from borders); strides alias. Global pooling at the end gives approximate invariance.
- "What limits im2col?" — Memory: about times the input. Libraries use implicit GEMM, Winograd or FFT.
10. Practice #
The notebook for this lesson runs in Colab (NumPy and a small PyTorch model on a CPU). It has five exercises, each with a check cell and a folded solution:
conv_output_size,conv_paramsandconv_macs, checked againstnn.Conv2d.im2colandconv2d_forwardin NumPy, checked againstF.unfoldandF.conv2d.col2imandconv2d_backward, checked against autograd and the adjoint identity.receptive_fieldfor a list of layers, checked against the support of a gradient.equivariance_error: test shift equivariance for stride 1 and stride 2.
At the end, a demo trains a tiny CNN and an MLP with the same budget on the scikit-learn digits and tests both on shifted test images.
11. Watch and read #
- Simon J. D. Prince, Understanding Deep Learning (free PDF): ch. 10 (§10.1 invariance and equivariance, §10.2–10.3 1-D and 2-D convolution, §10.4 downsampling, upsampling and transposed convolution) and §11.5 (ResNet and bottleneck blocks).
- Stanford CS231n notes, Convolutional Neural Networks: "Convolutional Layer" (with "Implementation as Matrix Multiplication") and "ConvNet Architectures".
- Dumoulin and Visin, A guide to convolution arithmetic for deep learning: ch. 2 (output sizes), ch. 4 (transposed convolution) and §5.1 (dilation).
- He, Zhang, Ren and Sun, Deep Residual Learning for Image Recognition: §3.3 and "Deeper Bottleneck Architectures" in §4.1 (Fig. 5).
- Ronneberger, Fischer and Brox, U-Net.
- Luo, Li, Urtasun and Zemel, Understanding the Effective Receptive Field in Deep Convolutional Neural Networks.
- Odena, Dumoulin and Olah, Deconvolution and Checkerboard Artifacts (Distill).
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.