Convolutional neural networks (CNNs)
How CNNs see — convolution, kernels, feature maps, stride, padding, and pooling — built from scratch in NumPy, with an interactive filter you scan across a real image.
What you'll learn
- Why a fully-connected layer is the wrong tool for an image
- How a convolution slides one small kernel to build a feature map
- What stride, padding, channels, and pooling actually do
- How stacking convolutions grows the receptive field into a hierarchy
Before you start
Before transformers ate the world, convolutional neural networks were how machines learned to see — and they still are the default for most vision tasks that aren’t worth a giant model. The whole idea is one small trick, repeated. Let’s earn it from first principles.
Edge detect: fires only where brightness changes — the rim of the disk. Output cell (3,3) = Σ (its 3×3 window × the kernel) = 0.00.
One kernel sweeps the whole image — that weight-sharing is why a CNN needs so few parameters. Click any feature-map cell to see its receptive field.
Why not just use a dense layer?
Take a modest 224×224 colour image. Flatten it and feed it to a fully connected layer with 1,000 units. That first weight matrix alone is 224 × 224 × 3 × 1000 ≈ 150 million parameters — for one layer, on a small image. Worse, it learns nothing reusable: a cat in the top-left corner and the same cat in the bottom-right are, to a dense layer, two completely unrelated input patterns. It would have to re-learn “cat” in every position.
Two facts about images break the dense approach, and point straight at the fix:
- Locality. A pixel’s meaning comes from its neighbours, not from a pixel 200 rows away. Edges, corners, textures are local.
- Translation invariance. An edge is an edge wherever it appears. The feature detector should be the same everywhere.
Convolution, concretely
A kernel is a small matrix of weights. You place it over a patch of the image, multiply element-wise, and sum — one number. Slide it one step, repeat. The grid of results is a feature map: high where the patch matched the kernel, low where it didn’t.
Hand-designed filters make it concrete: an edge-detector lights up only the rim of a shape, a blur smears it, identity copies it — same input, wildly different feature maps depending on the kernel. The window slides over every patch, filling the map one dot product at a time:
The magic of CNNs is that you don’t hand-pick these numbers. The nine kernel weights are parameters — gradient descent discovers which filters are worth having. Early layers reliably learn edge and colour detectors (the network rediscovers Sobel on its own); deeper layers learn textures, then parts, then objects.
Build it from scratch
Here is a valid 2-D convolution in pure NumPy — no deep-learning library,
just the multiply-and-sum. Run it, then change the kernel to the blur
np.ones((3,3))/9 and watch the output smooth out.
import numpy as np
# A tiny 6x6 "image" with a bright square in the middle
img = np.zeros((6, 6))
img[2:4, 2:4] = 1.0
# A vertical-edge detector (Sobel-x)
kernel = np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]])
def conv2d(x, k):
kh, kw = k.shape
H, W = x.shape
out = np.zeros((H - kh + 1, W - kw + 1))
for i in range(out.shape[0]):
for j in range(out.shape[1]):
patch = x[i:i+kh, j:j+kw] # the receptive field
out[i, j] = np.sum(patch * k) # one dot product
return out
fmap = conv2d(img, kernel)
print("input shape :", img.shape)
print("output shape:", fmap.shape, " (valid conv shrinks by kernel-1)")
print(np.round(fmap, 1))
# The +/- pair shows the left and right edges of the square.
input shape : (6, 6)
output shape: (4, 4) (valid conv shrinks by kernel-1)
[[ 1. 1. -1. -1.]
[ 3. 3. -3. -3.]
[ 3. 3. -3. -3.]
[ 1. 1. -1. -1.]]
Notice the output is smaller than the input (6×6 → 4×4). A 3×3 kernel
can’t centre on the border pixels, so a “valid” convolution trims a
kernel − 1 rim. Two knobs control this:
- Padding — add a border of zeros so the output keeps the input size
(
padding=1for a 3×3 kernel → “same” convolution). Without it, a deep stack would shrink the image away to nothing. - Stride — how far the kernel jumps each step. Stride 2 skips every other position, halving the output size — a cheap way to downsample.
Channels: stacking filters
One kernel finds one kind of feature. Real layers learn many in
parallel — say 64 kernels — producing 64 feature maps stacked into a
64-channel output. The next layer’s kernels are then 3×3×64: they look
across all incoming channels at once, combining “edge here + colour there”
into richer features. A colour input image is just the first case of this:
3 channels (R, G, B) in, out_channels feature maps out.
Pooling: throw away position, keep the signal
After a convolution + activation (usually ReLU), CNNs often pool: take each small region of the feature map and keep only its maximum (max pooling) or average. A 2×2 max-pool halves height and width and says “an edge fired somewhere in this region — I don’t care exactly where.” That deliberate loss of precise position is what buys translation invariance, and it shrinks the feature maps so deeper layers stay cheap.
The hierarchy: why depth works
Stack the block — conv → ReLU → pool — a few times and something powerful emerges. Each pooling step means a single deep-layer pixel now summarises a large patch of the original image: its receptive field grows. So the network builds a hierarchy:
pixels → edges → textures → motifs → object parts → objects
(layer 1) (layer 2) (layer 3) (deeper) (head)
Early layers see tiny patches and learn generic edges; late layers see most of the image and learn “this is a wheel, that is a face.” A final dense layer (or global-average-pool) turns the top feature maps into class scores. That’s a CNN — LeNet, AlexNet, VGG, and ResNet are all this block, stacked deeper with better training tricks.
In one breath
- A dense layer on an image is hopeless — ~150M parameters for one small image, and it re-learns each feature in every position.
- A convolution learns one tiny kernel and slides it everywhere (weight sharing), exploiting locality and translation invariance — a few thousand params that work at any resolution.
- Stride and padding control output size; many kernels stack into channels; pooling drops exact position to buy translation invariance and downsample.
- Stacking conv → ReLU → pool grows each unit’s receptive field, building a hierarchy: edges → textures → parts → objects.
- ResNet-style CNNs are still the pragmatic default for limited-data, low-latency vision; ViTs win at large data but need much more of it.
Check yourself
Quick check
What to remember
- A convolution slides one small learned kernel across the whole input — weight sharing turns a 150M-parameter dense layer into a few thousand reusable weights that work at any resolution.
- Stride and padding control output size; channels stack many filters; pooling trades exact position for translation invariance.
- Stacking
conv → ReLU → poolgrows the receptive field, building a hierarchy from edges to objects — the reason depth matters in vision.
Questions about this lesson
Why use a CNN instead of a fully-connected network for images?
A dense layer learns a separate weight for every pixel-to-unit pair, so even a small image needs hundreds of millions of parameters and has to re-learn each feature in every position. A convolution learns one small kernel and slides it everywhere, so the same few weights detect a feature wherever it appears. That weight-sharing exploits the two facts that matter for images — locality and translation invariance — making the network both far smaller and far more data-efficient.
What is a feature map in a CNN?
A feature map is the output of sliding one kernel across the whole input: a grid whose value at each position is the dot product of the kernel with the patch underneath it. It is high where the input matched the kernel's pattern (an edge, a texture) and low elsewhere — literally a map of where that feature was found. A convolutional layer with 64 kernels produces 64 feature maps, stacked as 64 output channels.
What do stride and padding do in a convolution?
Stride is how far the kernel jumps each step: stride 1 visits every position, stride 2 skips every other one and halves the output size (cheap downsampling). Padding adds a border of zeros around the input so the kernel can centre on edge pixels; 'same' padding (1 pixel for a 3×3 kernel) keeps the output the same size as the input, which is what stops a deep stack from shrinking the image away to nothing.
How does a receptive field explain why deep CNNs work?
A receptive field is the region of the original image that influences one unit's value. Early layers have tiny receptive fields and can only see edges; each pooling or strided step enlarges the receptive field, so deep units summarise large patches and can represent whole object parts. Stacking conv→ReLU→pool therefore builds a hierarchy — edges → textures → parts → objects — which is the reason depth helps in vision.
Practice this in an interview
All questionsEach filter has k*k*C_in weights plus one bias, and a layer with C_out filters therefore has (k*k*C_in + 1)*C_out parameters. This count is independent of the input's spatial dimensions H and W, which is what makes CNNs so parameter-efficient.
A filter (kernel) is the set of learned weights that the network applies at each spatial position; a feature map is the spatial grid of responses produced when one filter slides over the input. Each filter detects one type of pattern, and the full stack of feature maps across all filters constitutes the layer's output representation.
Pooling downsamples a feature map by aggregating values in a local window, reducing spatial dimensions and building position tolerance. Max pooling takes the strongest activation in each window; average pooling takes the mean. Max pooling dominates in classification backbones; average pooling is preferred in global summarisation and smooth feature maps.
A 1x1 convolution applies a learned linear combination across channels at each spatial position, without looking at any spatial neighbourhood. It is used to change the number of channels cheaply, add non-linearity between pointwise operations, and build the bottleneck blocks at the core of Inception and ResNet-50+.
The receptive field is the region in the original input that influences a single activation in a given layer. With a 3x3 kernel and stride 1, each layer adds 2 pixels to the receptive field in each direction; stacking L layers gives a theoretical receptive field of (2L+1) x (2L+1). Stride, pooling, and dilated convolutions all expand the receptive field faster.
ResNet (He et al., 2015) stacks residual blocks that add the input directly to the block's output, enabling stable training of 50–152+ layer networks. The key design choices are the residual shortcut for gradient flow, batch normalisation after each convolution, and bottleneck blocks (1x1 → 3x3 → 1x1) that control parameter count in deeper variants.
Skip connections add the input of a block directly to its output — bypassing the conv-BN-ReLU stack — so gradients can flow straight back to early layers without passing through every weight matrix. They solved the degradation problem that made deeper plain networks perform worse than shallower ones, not because of overfitting but because of optimisation difficulty.
Stride sets how many positions the kernel jumps between applications, controlling output resolution — stride 2 roughly halves spatial dimensions. Padding adds values (usually zeros) around the border so the kernel can be applied to edge pixels, letting you choose whether the output shrinks, stays the same, or (rarely) grows relative to the input.
A convolution slides a small learned weight matrix (kernel) across the input, computing a dot product at each position to produce a feature map. Each kernel learns to detect one spatial pattern — an edge, a corner, a texture — regardless of where it appears in the image.
CNNs exploit three structural properties of images — local correlation, translation invariance, and compositional hierarchy — through parameter sharing and local receptive fields. A dense network treats every pixel as independent, ignoring spatial structure and requiring orders of magnitude more parameters.