Module 3 — Pooling and spatial downsampling
Module 2 showed one way to shrink a feature map: a strided convolution. Pooling is the other way, and for a long time it was the default. Both cut the spatial resolution in half; they differ in what they preserve and what they forget. This module makes the trade-off explicit, introduces global average pooling as a modern replacement for the classical dense head, and closes the small CNN we started building on CIFAR-10.
Max pooling: keep the strongest response
A 2 by 2 max pooling with stride 2 slides a 2 by 2 window over the feature map, outputs the maximum of the four values, and moves two pixels to the right. Applied to a 32 by 32 feature map, it produces a 16 by 16 map, one that has divided the number of activations by four while keeping the strongest local response.
import numpy as np
fmap = np.array([
[1, 3, 2, 4],
[5, 6, 1, 2],
[7, 8, 9, 0],
[4, 3, 2, 1],
], dtype=float)
def maxpool2d(x, k=2, s=2):
h, w = x.shape
out = np.zeros((h // s, w // s))
for i in range(out.shape[0]):
for j in range(out.shape[1]):
out[i, j] = x[i*s:i*s+k, j*s:j*s+k].max()
return out
print(maxpool2d(fmap))
# [[6. 4.]
# [8. 9.]]
The intuition: after a convolution layer, a large activation means the filter fired somewhere in that neighbourhood. Max pooling keeps the fact that the filter fired and forgets exactly where it fired within the window. That partial forgetting is the whole point — it makes the representation approximately robust to small shifts.
Average pooling: preserve the intensity
Average pooling replaces the max by the mean over the window. It preserves the overall activation level of the region and smooths noise, which is why it appears at the top of networks like GoogLeNet and ResNet, and in modern segmentation heads. In the middle of a network, max pooling usually wins on discriminative tasks because it selects the strongest feature; average pooling tends to blur.
Global average pooling replaces the flatten + dense head
Classical CNNs — LeNet and AlexNet, as we will see in the next module — end with a Flatten followed by two or three dense layers. That head can hold tens of millions of parameters, most of the model, and overfits easily on small datasets.
GlobalAveragePooling2D proposes a radical alternative: after the last convolutional block, collapse each feature map to its spatial average. A block of 512 feature maps at 7 by 7 becomes a vector of 512 numbers — one number per channel — that goes straight to a softmax classifier with parameters. No flatten, almost no head.
import tensorflow as tf
def small_cnn_cifar():
x = tf.keras.Input(shape=(32, 32, 3))
h = tf.keras.layers.Conv2D(32, 3, padding="same", activation="relu")(x)
h = tf.keras.layers.Conv2D(32, 3, padding="same", activation="relu")(h)
h = tf.keras.layers.MaxPooling2D(2)(h) # 16 x 16
h = tf.keras.layers.Conv2D(64, 3, padding="same", activation="relu")(h)
h = tf.keras.layers.Conv2D(64, 3, padding="same", activation="relu")(h)
h = tf.keras.layers.MaxPooling2D(2)(h) # 8 x 8
h = tf.keras.layers.Conv2D(128, 3, padding="same", activation="relu")(h)
h = tf.keras.layers.GlobalAveragePooling2D()(h) # (None, 128)
out = tf.keras.layers.Dense(10, activation="softmax")(h)
return tf.keras.Model(x, out)
model = small_cnn_cifar()
model.summary()
Run it: this network has roughly 130 000 parameters and reaches around 75 % test accuracy on CIFAR-10 after 30 epochs. A flat + dense head on the same body would push the parameter count above one million for the same body — and often overfits, giving worse test accuracy.
By averaging over every position, GAP forces the network to make each channel meaningful on its own: the classifier can no longer rely on a specific location. This is why it also plays the role of a Class Activation Map basis in module 10 — averaging preserves per-channel intensity.
Strided convolution versus pooling
At first sight, a strides=2 convolution and a 2 by 2 max pool both cut the size in half. In practice:
| Aspect | Max pooling | Strided convolution |
|---|---|---|
| Parameters | zero | ordinary conv parameters |
| What it selects | strongest local activation | a learned local summary |
| Bias | selection is fixed | selection is learned from data |
| Where it fits | after a conv layer, or at the head | inside residual blocks (module 6) |
Modern architectures (ResNet, EfficientNet) mostly use strided convolutions, because they let the network learn what to summarise. Pure vision transformers have removed pooling entirely. Pooling has not disappeared, but it is no longer the default: it appears where a fixed reduction rule is a feature, not a limitation.
Approximate translation invariance, not exact
A CNN with pooling is often described as "translation invariant". The truth is more nuanced. The convolution itself is equivariant: shift the input by one pixel, shift the output by one pixel. Pooling introduces approximate invariance to small shifts — a 2 by 2 max pool tolerates one-pixel jitter within the window, but a shift larger than the window changes the output.
Stack several pooling layers and the tolerated shift grows, but so does the model's sensitivity to aliasing: a stride-2 operation can turn a smooth translation of the input into a discontinuous jump of the output. This is one reason data augmentation (module 8) matters even for CNNs.
Convolution is equivariant to translation. Rotation, scale, viewpoint: no such property. If your CIFAR-10 cat is upside down and no upside-down cat is in the training set, the network will most likely miss it. Data augmentation is the only reliable answer, since architectural invariance to rotation is much harder to build in.
The PyTorch equivalent, in one call-out
For readers switching frameworks, the corresponding PyTorch layers are torch.nn.MaxPool2d(2), torch.nn.AvgPool2d(2) and torch.nn.AdaptiveAvgPool2d(1) — the latter being the equivalent of GlobalAveragePooling2D for a tensor. The concepts do not change; only the constructor names do.
In summary
- Max pooling keeps the strongest activation in each window, average pooling keeps the mean; both have zero parameters and shrink the spatial dimensions.
- GlobalAveragePooling2D replaces
Flattenplus dense layers by a per-channel average, cutting parameters by an order of magnitude and reducing overfitting. - Strided convolution is a learned alternative to pooling; modern architectures prefer it inside blocks and reserve pooling for the head.
- CNNs are approximately translation-invariant through pooling, but rotation and scale are not built in — augmentation, not architecture, is what compensates.
Next module: LeNet-5, reproduced in Keras, and what AlexNet added a decade later to make it work on natural images at scale.