Pooling Layers: Spatial Downsampling and Invariance
Learners will add MaxPool2d after convolution layers, compute output dimensions, and understand how pooling gives position tolerance and reduces computation.
Pooling Layers: Spatial Downsampling and Invariance is a free Machine Learning Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Machine Learning Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why Pooling Layers Exist
Pooling layers reduce the spatial dimensions of feature maps, decreasing computation and memory while making representations more compact. They also introduce a degree of spatial invariance — small translations of a feature in the input produce the same pooled output. Without pooling (or stride-2 convolutions), the spatial dimensions would remain constant through all layers, making the network computationally prohibitive for large images.
import torch
import torch.nn as nn
# Without pooling: feature map grows in channels but not reduced
conv1 = nn.Conv2d(3, 32, 3, padding=1) # (B, 32, H, W)
conv2 = nn.Conv2d(32, 64, 3, padding=1) # (B, 64, H, W)
# With pooling: spatial dims are halved each time
pool = nn.MaxPool2d(kernel_size=2, stride=2)
x = torch.randn(4, 3, 32, 32)
out = pool(torch.relu(conv1(x)))
print(out.shape) # (4, 32, 16, 16) -- halved!Max Pooling: Taking the Maximum
MaxPool2d divides the input feature map into non-overlapping windows and takes the maximum value in each window. The maximum corresponds to the strongest activation of the filter at any position in that window — it captures whether the feature was present anywhere in the region, regardless of its exact position. A 2x2 max pool with stride 2 reduces height and width by half, cutting the total spatial size by 4x.
import torch
import torch.nn as nn
pool = nn.MaxPool2d(kernel_size=2, stride=2)
# Simple 4x4 feature map
x = torch.tensor([[[[ 1., 3., 2., 4.],
[ 5., 6., 7., 8.],
[ 9., 2., 1., 3.],
[ 4., 5., 6., 7.]]]])
out = pool(x)
print(out.shape) # (1, 1, 2, 2)
print(out)
# Max of top-left 2x2: max(1,3,5,6)=6
# Max of top-right 2x2: max(2,4,7,8)=8
# Max of bottom-left 2x2: max(9,2,4,5)=9
# Max of bottom-right 2x2: max(1,3,6,7)=7Average Pooling vs Max Pooling
AvgPool2d takes the average of values in each pooling window instead of the maximum. Average pooling smooths the feature map and retains information from all activations, while max pooling discards the weaker activations. Max pooling is preferred in classification networks where the presence of a feature matters more than its average strength. Average pooling is commonly used for global pooling at the end of a CNN to collapse spatial dimensions.
import torch
import torch.nn as nn
x = torch.tensor([[[[ 1., 3., 2., 4.],
[ 5., 6., 7., 8.],
[ 9., 2., 1., 3.],
[ 4., 5., 6., 7.]]]])
max_pool = nn.MaxPool2d(2, stride=2)
avg_pool = nn.AvgPool2d(2, stride=2)
print('MaxPool:', max_pool(x))
# tensor([[[[6., 8.], [9., 7.]]]])
print('AvgPool:', avg_pool(x))
# tensor([[[[3.75, 5.25], [5.00, 4.25]]]])Global Average Pooling
Global Average Pooling (GAP) collapses the entire spatial dimension to a single value per channel by averaging all spatial positions. For a feature map of shape (B, C, H, W), GAP produces (B, C) — a vector of C values representing the average activation of each filter across the entire image. GAP replaces the flatten + large fully connected layers at the top of classic CNNs (VGG), drastically reducing parameters and acting as a powerful regulariser. It is used in ResNet, MobileNet, and EfficientNet.
import torch
import torch.nn as nn
# Global Average Pooling (AdaptiveAvgPool2d to any output size)
gap = nn.AdaptiveAvgPool2d(output_size=(1, 1))
x = torch.randn(8, 512, 7, 7) # typical final feature map
out = gap(x)
print(out.shape) # (8, 512, 1, 1)
# Flatten to (B, C) for the classifier
flat = out.view(out.size(0), -1)
print(flat.shape) # (8, 512)
# Then: nn.Linear(512, num_classes)Computing Output Dimensions
Computing the output spatial dimensions after pooling is straightforward: output_size = floor((input_size - kernel_size) / stride) + 1. For a 2x2 pool with stride 2 on a 28x28 input, the output is 14x14. Misjudging dimensions is a common source of shape errors. AdaptiveMaxPool2d and AdaptiveAvgPool2d solve this by accepting the desired output size directly, automatically computing the required kernel and stride — ideal when you want a fixed output regardless of input size.
import torch
import torch.nn as nn
# Classic fixed-size pooling
x = torch.randn(1, 32, 28, 28)
pool = nn.MaxPool2d(kernel_size=2, stride=2)
print(pool(x).shape) # (1, 32, 14, 14)
# Adaptive pooling: specify desired output size
adaptive_pool = nn.AdaptiveAvgPool2d((4, 4))
print(adaptive_pool(x).shape) # (1, 32, 4, 4) -- any input
# Works even with irregular input sizes:
x2 = torch.randn(1, 32, 17, 23)
print(adaptive_pool(x2).shape) # (1, 32, 4, 4) -- still 4x4!Pooling for Translation Invariance
Pooling provides local translation invariance: if a feature shifts by a few pixels, it likely still falls within the same pooling window and produces the same maximum activation. This means the network recognises a cat whether it is slightly to the left or right in the image. However, pooling is only locally invariant — large translations still produce different outputs. Data augmentation (random crops, flips) is needed to achieve global translation invariance.
import torch
import torch.nn as nn
pool = nn.MaxPool2d(2, 2)
# Feature map with an 'activated' pixel at position (1,1)
fm1 = torch.zeros(1, 1, 4, 4)
fm1[0, 0, 1, 1] = 1.0 # activation at (1,1)
# Shift by 1 pixel to (1,2)
fm2 = torch.zeros(1, 1, 4, 4)
fm2[0, 0, 1, 2] = 1.0
# Both fall in the same 2x2 window -> same pool output
print(torch.allclose(pool(fm1), pool(fm2))) # True!Pooling vs Stride-2 Convolutions
Modern CNN architectures (ResNet, EfficientNet) increasingly use stride-2 convolutions instead of separate max pooling layers. The advantage is that stride-2 convolutions are learnable — the network decides how to downsample, potentially preserving more useful information than the fixed max operation. Max pooling is still used in classic architectures (VGGNet, AlexNet) and for its computational simplicity. Both approaches achieve the same goal: reducing spatial resolution by a factor of 2.
import torch
import torch.nn as nn
# Option 1: Conv + explicit MaxPool
block_maxpool = nn.Sequential(
nn.Conv2d(32, 64, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, 2) # separate pooling
)
# Option 2: Stride-2 Conv (learnable downsampling)
block_stride = nn.Sequential(
nn.Conv2d(32, 64, 3, padding=1, stride=2), # stride=2
nn.ReLU()
)
x = torch.randn(4, 32, 16, 16)
print(block_maxpool(x).shape) # (4, 64, 8, 8)
print(block_stride(x).shape) # (4, 64, 8, 8) -- same!Pooling Has No Learnable Parameters
An important property of pooling layers is that they have no learnable parameters. Max pooling simply selects the maximum; average pooling computes the mean. This makes pooling layers very fast and memory-free — they do not contribute to the parameter count. The lack of parameters also means pooling cannot overfit. This is why global average pooling at the end of a network is a strong regulariser — it forces the network to encode information in each channel's average, not in spatial positions.
import torch.nn as nn
pool = nn.MaxPool2d(2, 2)
avg_pool = nn.AvgPool2d(2, 2)
# Count parameters
max_params = sum(p.numel() for p in pool.parameters())
avg_params = sum(p.numel() for p in avg_pool.parameters())
print('MaxPool parameters:', max_params) # 0
print('AvgPool parameters:', avg_params) # 0
# Compare with a Conv2d layer
conv = nn.Conv2d(32, 32, 2, stride=2) # similar operation
conv_params = sum(p.numel() for p in conv.parameters())
print('Conv2d (stride-2) parameters:', conv_params) # 4128Typical CNN Architecture with Pooling
A standard CNN architecture alternates between convolutional blocks (Conv+BN+ReLU) and pooling layers, progressively reducing spatial dimensions while increasing channel count. After the convolutional blocks, global average pooling collapses spatial dimensions, and a linear layer maps to class scores. This pattern produces a network with manageable parameter count, good generalisation, and strong image classification performance.
import torch
import torch.nn as nn
class SimpleCNN(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 32, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2), # 32->16
nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(2), # 16->8
nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(),
)
self.pool = nn.AdaptiveAvgPool2d((1, 1)) # GAP
self.classifier = nn.Linear(128, num_classes)
def forward(self, x):
x = self.features(x)
x = self.pool(x).view(x.size(0), -1)
return self.classifier(x)
model = SimpleCNN()
x = torch.randn(4, 3, 32, 32)
print(model(x).shape) # (4, 10)Fractional and Adaptive Pooling
AdaptiveMaxPool2d(output_size) automatically computes the kernel size and stride needed to produce the specified output dimensions, regardless of input size. This is invaluable when you want a fixed-size vector for the classifier but the input images have varying sizes (e.g., object detection, variable-resolution datasets). FractionalMaxPool2d uses randomised pooling regions to add stochasticity, acting as an additional regulariser in some architectures.
import torch
import torch.nn as nn
# Adaptive pooling: always produces 3x3 output
adaptive = nn.AdaptiveMaxPool2d((3, 3))
sizes = [(32, 32), (48, 64), (100, 100)]
for h, w in sizes:
x = torch.randn(2, 64, h, w)
out = adaptive(x)
print(f'Input {h}x{w} -> Output {out.shape[2]}x{out.shape[3]}')
# Always 3x3 regardless of input!
# Fractional max pooling (randomised pooling regions)
frac = nn.FractionalMaxPool2d(kernel_size=2, output_size=(6, 6))
x = torch.randn(2, 32, 12, 12)
print('Fractional pool:', frac(x).shape) # (2, 32, 6, 6)Visualising the Effect of Pooling
To understand what pooling does visually: a feature map before max pooling shows the raw filter response at each pixel. After pooling, the output is spatially coarser — each value represents the strongest response in its neighbourhood. The feature map becomes more abstract and position-independent. Deep in a network, after many rounds of pooling, each neuron's receptive field covers most of the original image, enabling global pattern recognition.
import torch
import torch.nn as nn
# Simulate pooling effect on a feature map
pool2 = nn.MaxPool2d(2, 2)
pool4 = nn.MaxPool2d(4, 4)
x = torch.randn(1, 1, 16, 16)
print('Before pooling:', x.shape) # (1,1,16,16)
print('After 2x2 pool:', pool2(x).shape) # (1,1,8,8)
print('After 4x4 pool:', pool4(x).shape) # (1,1,4,4)
# After 3 rounds of 2x2 pooling on 32x32 input:
pool_3x = nn.Sequential(
nn.MaxPool2d(2), nn.MaxPool2d(2), nn.MaxPool2d(2)
)
print('After 3x 2x2 pool:', pool_3x(torch.randn(1,1,32,32)).shape)
# (1, 1, 4, 4)Quick Check
Test your understanding of Machine Learning with Python concepts from this lesson.
Lesson Recap
In this lesson you learned: MaxPool2d and AvgPool2d reduce spatial dimensions by taking the max or average in each pooling window, providing compact representations and local translation invariance, AdaptiveAvgPool2d produces a fixed output size regardless of input dimensions (used as Global Average Pooling), and pooling has no learnable parameters, making it fast and non-overfitting. Next up we build and train a complete CNN on CIFAR-10.
Frequently asked questions
Is the “Pooling Layers: Spatial Downsampling and Invariance” lesson free?
Yes — the full text of “Pooling Layers: Spatial Downsampling and Invariance” is free to read here on the web, and the Machine Learning Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Machine Learning Academy course, upgrade to CoddyKit PRO.
What will I learn in “Pooling Layers: Spatial Downsampling and Invariance”?
Learners will add MaxPool2d after convolution layers, compute output dimensions, and understand how pooling gives position tolerance and reduces computation. You practise Machine Learning Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Machine Learning Academy?
No prior experience is required. Machine Learning Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Pooling Layers: Spatial Downsampling and Invariance” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Machine Learning Academy lesson?
Yes. Every Machine Learning Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Convolution and Filters: Detecting Edges and Patterns
- Pooling Layers: Spatial Downsampling and Invariance
- Building and Training a CNN on CIFAR-10
- Data Augmentation: Transforms for Robustness