Convolutional networks are built from one operation repeated with different kernels, strides and dilations, and most design decisions about them come down to a single number: how many input pixels can influence one output value. This post implements the operation in NumPy, works out the receptive-field arithmetic, runs a Sobel edge detector on a synthetic image, and then does the parameter count that made VGG-style architectures abandon large kernels.
The operation
What deep-learning libraries call a convolution is, strictly, a cross-correlation: the kernel is slid over the input without being flipped. For a single-channel image
with stride
import numpy as np
def conv2d(x, k, stride=1, padding=0):
"""Cross-correlation of a 2-D array with a 2-D kernel (what CNNs call 'convolution')."""
if padding:
x = np.pad(x, padding)
H, W = x.shape
kh, kw = k.shape
oh, ow = (H - kh) // stride + 1, (W - kw) // stride + 1
out = np.zeros((oh, ow))
for i in range(kh): # k*k shifted, strided slices, each weighted
for j in range(kw):
out += k[i, j] * x[i:i + stride * oh:stride, j:j + stride * ow:stride]
return out
For a padding=1, stride=2, padding=1, matching the formula.
TIP
Because the kernel is not flipped, a learned CNN kernel is "the pattern the unit responds to", read directly. If you ever port a hand-designed filter from signal-processing literature, which assumes true convolution, flip it in both axes first; for symmetric kernels such as Gaussians the distinction does not matter, for Sobel-type derivative kernels it flips the sign.
Edge detection on a synthetic image
A
sx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], float)
gx = conv2d(img, sx, padding=1)
gy = conv2d(img, sx.T, padding=1)
mag = np.hypot(gx, gy)
Two details carry over to learned networks. First, the sign of the response matters: a bright-to-dark step and a dark-to-bright step give opposite signs, which is why networks follow convolutions with a non-linearity rather than taking the magnitude. Second, the ramp shows that a derivative kernel responds to gradients, not to objects; "edges" are a property of the kernel's shape, not of the scene.
Receptive-field arithmetic
The receptive field of a unit is the set of input pixels that can change its value. For a stack of layers with kernel sizes
Where the recurrence comes from
Think of the output of layer
With stride 1 and no dilation the recurrence is linear:
def receptive_field(kernels, strides, dilations=None):
dilations = dilations or [1] * len(kernels)
r, jump, out = 1, 1, [1]
for k, s, d in zip(kernels, strides, dilations):
r += (k - 1) * d * jump
jump *= s
out.append(r)
return out
The dilated configuration (
Why two 3×3 layers instead of one 5×5
Two stacked
| Receptive field | Option | Weights | C=64 | C=256 | Non-linearities |
|---|---|---|---|---|---|
| 5 × 5 | one 5 × 5 | 102,400 | 1,638,400 | 1 | |
| 5 × 5 | two 3 × 3 | 73,728 | 1,179,648 | 2 | |
| 7 × 7 | one 7 × 7 | 200,704 | 3,211,264 | 1 | |
| 7 × 7 | three 3 × 3 | 110,592 | 1,769,472 | 3 |
The stacked option has fewer parameters and more non-linearities, so it is both cheaper and more expressive; the price is that the composed linear map is constrained (a product of two
The effective receptive field is smaller than the arithmetic says
The recurrence counts every pixel that can influence the output. How much each pixel actually influences it is a different question. Repeatedly applying a
Luo et al. measured this on trained networks and found the same Gaussian-like profile, with an effective radius growing like
Summary
-
conv2dis a sum of shifted, strided, weighted slices; that view also explains the output-size formula. - Receptive fields grow additively with kernel size and multiplicatively with stride and dilation.
- Two
layers replace one with instead of weights and one more non-linearity; three replace a with instead of . - The effective receptive field is Gaussian-shaped and considerably smaller than the nominal one.
Further reading
- Simonyan and Zisserman (2014), Very deep convolutional networks for large-scale image recognition (VGG): https://arxiv.org/abs/1409.1556
- Dumoulin and Visin (2016), A guide to convolution arithmetic for deep learning: https://arxiv.org/abs/1603.07285
- Luo et al. (2016), Understanding the effective receptive field in deep convolutional neural networks: https://arxiv.org/abs/1701.04128
- Araujo, Norris and Sim (2019), Computing receptive fields of convolutional neural networks, Distill: https://distill.pub/2019/computing-receptive-fields/
- Yu and Koltun (2015), Multi-scale context aggregation by dilated convolutions: https://arxiv.org/abs/1511.07122
- PyTorch documentation for
torch.nn.Conv2d, including the output-size formula: https://pytorch.org/docs/stable/generated/torch.nn.Conv2d.html