Discord

6Computer Vision 6.2Computer Vision Theory

6.2.2 Pooling

Shrink feature maps by summarising small windows: max and average pooling by hand and in PyTorch, why networks pool, global average pooling, and strided convolutions.

Why learn pooling?

In Convolutional Layers, we saw that a layer of 16 kernels with padding turned the macaw photo into 16 feature maps of 430 × 500, over three million numbers from a single layer. A network stacks many such layers, and if every feature map stayed the size of the image, the network would be slow and would need a great deal of memory.

Pooling shrinks feature maps on purpose. It slides a small window over each map, as a convolution does, but instead of multiplying by learned weights it simply summarises the window: by its largest value (max pooling) or by its mean (average pooling). A 2 × 2 window that jumps 2 pixels at a time halves the height and the width. Pooling has no parameters at all, and it appears in almost every classic image network, including the ones in Convolutional Neural Networks.

What you need to know

By the end of this module, you should be comfortable with these ideas:

Idea In one line
Max pooling Replace each window by its largest value.
Average pooling Replace each window by the mean of its values.
Window and stride Usually a 2 × 2 window with stride 2, which halves the height and the width.
Channels Each channel is pooled on its own, so the number of channels does not change.
No parameters Pooling has nothing to learn.
nn.MaxPool2d, nn.AvgPool2d PyTorch's pooling layers; the output size follows the same formula as a convolution.
Why pool Smaller maps, some tolerance to small shifts, and more of the image behind each value.
Global average pooling nn.AdaptiveAvgPool2d(1) averages each whole channel to one number, for any image size.
Strided convolutions A learned alternative to pooling that many modern networks use.

Learn it properly

Pooling is the simplest layer in an image network, and one course section covers it. Read it, then use this page to see the numbers and the shapes.

Pick one main course

One short resource is enough here.

Our pick: Dive into Deep Learning, section 7.5

Read Pooling, section 7.5 of Dive into Deep Learning, which follows straight on from the sections recommended in Convolutional Layers. It explains why pooling makes a network less sensitive to exactly where a pattern is, computes max and average pooling by hand and with a short function, and then shows padding, stride and several channels with PyTorch's pooling layer. Choose the PyTorch tab for the code.

If you prefer something else

  • Course notes: the Pooling Layer section of Stanford CS231n's Convolutional Neural Networks notes is short, with a picture of max pooling, and mentions the move towards networks that use few or no pooling layers.
  • A video: deeplizard's Max Pooling in Convolutional Neural Networks explained works through examples of max pooling and explains why it is used. Watch the first seven minutes; the code demo at the end uses Keras, which the contest machines do not have.

A quick tour

The rest of this page pools the feature map we computed by hand in Convolutional Layers, then looks at the pooling layers in PyTorch and at why networks use them. Go through your chosen course first, then run each cell in a notebook as you read.

Max pooling by hand

Here is the 4 × 4 feature map that the vertical edge detector produced from our made-up 6 × 6 image in Convolutional Layers:

col 0 col 1 col 2 col 3
row 0 11 16 11 3
row 1 16 13 4 0
row 2 15 7 0 0
row 3 10 2 −1 0

The usual pooling window is 2 × 2 with a stride of 2, so the windows do not overlap: they cut the map into four blocks of four values. Max pooling keeps the largest value of each block. The top-left block holds 11, 16, 16 and 13, so its output is 16; the top-right block holds 11, 3, 4 and 0, so its output is 11. All four:

col 0 col 1
row 0 16 11
row 1 15 0

Average pooling

Average pooling keeps the mean of each block instead. The top-left block gives (11+16+16+13)/4=14(11 + 16 + 16 + 13) / 4 = 14:

col 0 col 1
row 0 14 4.5
row 1 8.5 −0.25

Both results are 2 × 2, a quarter of the size. Max pooling answers "how strongly does the pattern appear anywhere in this block?", while average pooling answers "how much of it is there on average?". For a feature map from an edge detector, the first is usually the more useful question, which is why max pooling is the more common choice between convolutional layers. Step through both:

Pooling in PyTorch

PyTorch has both as layers, nn.MaxPool2d and nn.AvgPool2d. The first argument is the window size, and the stride is the same as the window size unless you say otherwise. Like convolutional layers, they take batches of shape (N, C, H, W):

INPUTPython
import torch
from torch import nn

feature_map = torch.tensor([[11, 16, 11, 3],
                            [16, 13, 4, 0],
                            [15, 7, 0, 0],
                            [10, 2, -1, 0]], dtype=torch.float32)
feature_map = feature_map.unsqueeze(0).unsqueeze(0)   # (1, 1, 4, 4)

max_pool = nn.MaxPool2d(kernel_size=2)      # stride 2 by default
average_pool = nn.AvgPool2d(kernel_size=2)
print(max_pool(feature_map))
print(average_pool(feature_map))
[OUTPUT]
tensor([[[[16., 11.],
          [15.,  0.]]]])
tensor([[[[14.0000,  4.5000],
          [ 8.5000, -0.2500]]]])

The same tables as by hand. Pooling layers have no weights, so there is nothing to train:

INPUTPython
len(list(max_pool.parameters()))
[OUTPUT]
0

The effect on shape

Pooling works on each channel separately: values from one channel are never mixed with values from another. So the number of channels stays the same and only the height and width shrink. Here is a batch shaped like the 16 feature maps of the macaw photo from the last module:

INPUTPython
torch.manual_seed(0)
feature_maps = torch.rand(1, 16, 430, 500)
pooled = max_pool(feature_maps)
print(pooled.shape)
print(feature_maps.numel(), pooled.numel())
[OUTPUT]
torch.Size([1, 16, 215, 250])
3440000 860000

Four times fewer numbers for the next layer to process. The output size follows the same formula as a convolution, ⌊(H+2P−K)/S⌋+1\lfloor (H + 2P - K) / S \rfloor + 1, with the window size as KK: ⌊(430−2)/2⌋+1=215\lfloor (430 - 2) / 2 \rfloor + 1 = 215. When a size is odd, the last row or column does not fill a window and is simply dropped:

INPUTPython
odd = torch.rand(1, 1, 215, 250)
max_pool(odd).shape
[OUTPUT]
torch.Size([1, 1, 107, 125])

215 rows become 107, not 107.5: the last row was never looked at. That rarely matters, but it is why image networks often use sizes such as 32, 64 or 224, which can be halved many times without a remainder.

Why networks pool

Pooling throws information away, so it is worth being clear about what it buys. There are three things.

Smaller feature maps. Every 2 × 2 pooling layer divides the number of positions by four, and with it the computation and memory of every later layer. This is the main practical reason.

Tolerance to small shifts. Max pooling only reports the largest value in each window, not where in the window it was. If a pattern moves by a pixel but stays inside the same window, the pooled output does not change. We can see this with a feature map that has a single strong response:

INPUTPython
def one_spot(row, col):
    spot = torch.zeros(1, 1, 4, 4)
    spot[0, 0, row, col] = 9.0
    return spot

for row, col in [(0, 0), (1, 1), (0, 2)]:
    pooled_spot = max_pool(one_spot(row, col))
    print((row, col), pooled_spot[0, 0].tolist())
[OUTPUT]
(0, 0) [[9.0, 0.0], [0.0, 0.0]]
(1, 1) [[9.0, 0.0], [0.0, 0.0]]
(0, 2) [[0.0, 9.0], [0.0, 0.0]]

Moving the spot from (0, 0) to (1, 1) leaves the output unchanged, because both positions are in the top-left window. Moving it to (0, 2) crosses into the next window, and the output changes. So the tolerance is real but limited to shifts within a window; a stack of several pooling layers tolerates larger shifts. Photos of the same object are rarely aligned to the exact pixel, so this helps a classifier.

More context per value. After pooling, each value stands for a 2 × 2 block of the map before it, so a 3 × 3 kernel in the next layer covers what used to be 6 × 6. The region of the input image that can affect one value is called its receptive field. Two 3 × 3 convolutions in a row give each output a receptive field of 5 × 5 pixels. With a 2 × 2 pooling layer between them, it grows to 8 × 8, and with another pooling layer and a third convolution, to 18 × 18. Deep in a network, each value can therefore respond to a large part of the image, such as a whole eye or a whole beak, although every kernel is only 3 × 3.

Global average pooling

At the end of an image network, the feature maps have to become one row of numbers per image for the final nn.Linear layer. One way is to flatten them, but then the size of the nn.Linear depends on the size of the image. Global average pooling averages each channel over its whole map, so each channel becomes one number, whatever the height and width. In PyTorch it is nn.AdaptiveAvgPool2d(1), where 1 is the output size you ask for: 1 × 1.

INPUTPython
global_pool = nn.AdaptiveAvgPool2d(1)
for height, width in [(430, 500), (100, 80)]:
    maps = torch.rand(2, 16, height, width)
    pooled_maps = global_pool(maps)              # (2, 16, 1, 1)
    print(pooled_maps.shape, pooled_maps.flatten(1).shape)
[OUTPUT]
torch.Size([2, 16, 1, 1]) torch.Size([2, 16])
torch.Size([2, 16, 1, 1]) torch.Size([2, 16])

Both image sizes give 16 numbers per image, so the same classifier layer, nn.Linear(16, n_classes), works for both. flatten(1) keeps the batch dimension and joins the rest. The pooled value is exactly the mean of each channel:

INPUTPython
by_hand = maps.mean(dim=(2, 3))                  # mean over H and W
torch.allclose(pooled_maps.flatten(1), by_hand)
[OUTPUT]
True

"Adaptive" means that you give the output size and PyTorch works out the window size for you; nn.AdaptiveAvgPool2d(2) would give 2 × 2 per channel. Global average pooling has a second benefit: it has no parameters, while a flattened 16 × 107 × 125 map would need a classifier with over 200,000 weights per class.

Strided convolutions instead of pooling

A convolution with stride 2 also halves the height and width, as Convolutional Layers showed. The difference is that it learns how to summarise each region, instead of always taking the maximum or the mean. Many modern networks therefore shrink their feature maps mostly with strided convolutions.

OptionalSee pooling and stride in ResNet-18An optional look inside a larger image model; we will study models like this later.1 min readShowHide

torchvision's ResNet-18, a widely used image network, is an example. We can build it without downloading anything and look at a few of its layers:

INPUTPython
from torchvision.models import resnet18

resnet = resnet18()          # random weights, no download
print(resnet.maxpool)
print(resnet.layer2[0].conv1)
print(resnet.avgpool)
[OUTPUT]
MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
AdaptiveAvgPool2d(output_size=(1, 1))

ResNet-18 has a single max pooling layer, right after its first convolution. Every later time it halves the size of its feature maps, it uses convolutions with stride 2, like the one printed, and at the very end a global average pooling layer feeds its classifier.

Both styles work. For the small networks you will build first, a convolution followed by max pooling is simple and reliable, and it is what Convolutional Neural Networks uses.

Common mistakes

The messages are exactly what PyTorch prints.

Mistake What you see Fix
Pooling a map that is already smaller than the window RuntimeError: Given input size: (1x1x1). Calculated output size: (1x0x0). Output size is too small Use fewer pooling layers, or larger images; trace the shapes layer by layer.
An image or map without the batch and channel dimensions RuntimeError: non-empty 3D or 4D (batch mode) tensor expected for input Give pooling layers (N, C, H, W).
Feeding the output of nn.AdaptiveAvgPool2d(1) straight into nn.Linear RuntimeError: mat1 and mat2 shapes cannot be multiplied (64x1 and 16x10) Flatten first: nn.Flatten() or .flatten(1) turns (N, C, 1, 1) into (N, C).
Forgetting that each pooling layer halves the size No error until the first nn.Linear, then a shape mismatch Divide the height and width by 2 for every pooling layer when you compute the classifier's input size.
Odd sizes No error, but the last row or column is silently dropped Usually harmless; choose image sizes that divide evenly if it matters.

How to practise

There is no section on pooling on the practice sites we use (CodeChef, CS50 and HackerRank), so practise with small experiments:

  • Write max_pool_by_hand(feature_map) for a 2-dimensional tensor with two loops, like the convolve function in Convolutional Layers, and check it against nn.MaxPool2d(2).
  • Apply a 3 × 3 vertical edge kernel to the grayscale macaw photo, then max pooling, three times in a row. Show the result after each round with plt.imshow. How much of the photo can you still recognise?
  • Compare nn.MaxPool2d(2) with nn.MaxPool2d(kernel_size=3, stride=2, padding=1) on a map of 430 × 500. Predict both output shapes first with the formula.

Check yourself

What comes next

Resources

SourceTitleWhy read it
Dive into Deep Learning7.5 PoolingOur recommended starting point: max and average pooling, with padding, stride and channels, and PyTorch code.
Stanford CS231nConvolutional Neural Networks: Pooling LayerA short section on pooling, including the move towards networks without pooling layers.
deeplizardMax Pooling in Convolutional Neural Networks explainedA short video: what max pooling is, worked examples, and why it is used.
PyTorchtorch.nn.MaxPool2dThe reference page, with the output-size formula.

Finished this module? Mark it done.

Comments

Stuck on something, spotted a mistake, or found a resource that helped you? Tell us below.

Loading comments