Pooling & Spatial Hierarchy in CNNs
Press Next → or use ← → arrow keys
Why Pooling Exists
Pooling shrinks the spatial size, cuts computation, limits parameters downstream, and makes the network robust to small shifts — all with zero learnable parameters.
What Pooling Does
Slide a small window across the feature map and replace each window with a single summary number — the maximum, or the average. No weights, no learning, just a fixed reduction.
Convolution is equivariant — shift the input and the feature map shifts with it. Pooling adds invariance: it stops caring exactly where in a small region a feature was found.
Max Pooling vs Average Pooling
| Aspect | Max Pooling | Average Pooling |
|---|---|---|
| Rule | Maximum value in the window | Mean of the window |
| Answers | "Did this feature appear here?" | "How active is this region overall?" |
| Keeps | The strongest activation | The diffuse, background signal |
| Best for | Sharp features: edges, corners | Global context, final summaries |
| Seen in | AlexNet, VGG, ResNet | GoogLeNet GAP, MobileNet |
Inside a CNN's convolutional blocks, max pooling is the default — it preserves the "a feature fired here" signal. Average pooling shines at the very end, where you want a smooth global summary.
Worked Example — 2×2 Pool, Stride 2
Each coloured 2×2 window collapses to one number. Max pooling keeps the biggest value in each:
| Window | Values | Max | Average |
|---|---|---|---|
| Top-left | 1, 3, 5, 6 | 6 | 3.75 |
| Top-right | 2, 4, 1, 2 | 4 | 2.25 |
| Bottom-left | 3, 2, 1, 0 | 3 | 1.50 |
| Bottom-right | 4, 7, 6, 3 | 7 | 5.00 |
Translation Invariance
Small, local invariance is exactly what you want early in a network: features can wobble a few pixels without changing the summary. Stack pools and that tolerance compounds.
The Spatial Hierarchy — A Pyramid of Meaning
As pooling shrinks the map layer after layer, each neuron sees a bigger slice of the original image — and the features grow from simple to semantic:
Early layers detect edges and colour blobs; middle layers find corners and textures; deep layers recognise wheels, faces, and whole objects. Pooling stabilises each step of that climb.
Receptive Field — How Much a Neuron Sees
A neuron's receptive field is the patch of the original image it depends on. It grows with every layer — and every pooling step accelerates the growth.
For L stacked 3×3 conv layers, RF = 2L + 1. But a single 2×2 stride-2 pool doubles the effective field of everything after it — which is how deep CNNs come to "see" the whole image.
The Output-Size Formula
Pooling shrinks the map by a predictable amount:
| Input | Pool | Output | Reduction |
|---|---|---|---|
| 224×224 | 2×2, s2 | 112×112 | 75% fewer |
| 56×56 | 2×2, s2 | 28×28 | 75% fewer |
| 14×14 | 2×2, s2 | 7×7 | 75% fewer |
| 7×7 | 7×7 GAP | 1×1 | 98% fewer |
A 2×2 stride-2 pool halves each dimension: 224 → 112 → 56 → 28 → 14 → 7. Five pools take a big image down to a tiny, information-dense grid.
Global Average Pooling
At the end of a modern CNN, Global Average Pooling replaces the old flatten-plus-fully-connected block. It averages each entire feature map down to a single number per channel.
ResNet, EfficientNet, and MobileNet all end with GAP. It's lighter, resists overfitting, and feeds a clean vector straight into the classifier.
Pooling From Scratch & in PyTorch
The operation is a double loop of "grab a patch, take max or mean" — and one line each in PyTorch:
def pool2d(x, size=2, stride=2, mode='max'):
H, W = x.shape
OH = (H - size) // stride + 1
OW = (W - size) // stride + 1
out = np.zeros((OH, OW))
for i in range(OH):
for j in range(OW):
patch = x[i*stride:i*stride+size, j*stride:j*stride+size]
out[i, j] = patch.max() if mode=='max' else patch.mean()
return out
# PyTorch — the same three operations
max_pool = nn.MaxPool2d(kernel_size=2, stride=2)
avg_pool = nn.AvgPool2d(kernel_size=2, stride=2)
gap = nn.AdaptiveAvgPool2d((1, 1)) # global average pool
Run pool2d on our 4×4 map and it returns [[6, 4], [3, 7]] — exactly the max-pool result from the table.
Pooling Inside a Mini-CNN
Conv → ReLU → Pool, repeated, then GAP into a classifier — the standard modern shape:
class MiniCNN(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2, 2), # 28×28 → 14×14
nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2, 2), # 14×14 → 7×7
)
self.gap = nn.AdaptiveAvgPool2d((1, 1)) # 7×7 → 1×1
self.head = nn.Linear(64, num_classes)
def forward(self, x):
x = self.features(x)
x = self.gap(x).flatten(1) # (B, 64)
return self.head(x)
28×28 → 14×14 → 7×7 → 1×1, then a single linear layer to the classes. Each MaxPool halves the spatial size while ReLU and the next conv add richer features.
Everything in One Table
| Concept | Max Pool | Average Pool | Global Avg Pool |
|---|---|---|---|
| Operation | max(window) | mean(window) | mean(whole map) |
| Parameters | Zero | Zero | Zero |
| Preserves | Strongest activation | Overall energy | Channel global average |
| Invariance | Strong local | Moderate | Full spatial |
| Typical use | Conv blocks | Intermediate/final | Replaces flatten + FC |
| Output size | ⌊(N−F)/S⌋+1 | same | 1×1×C |
All three summarise a region into fewer numbers with zero parameters. Which statistic and which scope you choose is the only decision.
5 Golden Rules
Convolution finds features; pooling distils them and grows the view. Together they turn a grid of pixels into an understanding of what's in the picture.