Pooling Layers: Aggregation Layers in CNNs

Pooling Layers : Guide Complet — Couches de Regroupement dans les CNN

Pooling Layers: The Complete Guide to Aggregation Layers

Summary

Pooling layers are one of the fundamental building blocks of convolutional neural networks (CNNs). Their main role is to progressively reduce the spatial dimension of feature maps while preserving the most relevant information. This dimensionality reduction brings several crucial advantages: decrease in the number of parameters to learn, reduction of the risk of overfitting, and introduction of a certain invariance to small translations in the image.

In this complete guide, we will thoroughly explore the different types of pooling layers — max pooling, average pooling, adaptive pooling, fractional max pooling, and stochastic pooling — as well as their theoretical and practical implications. We will also provide concrete implementations in Python with PyTorch, accompanied by experimental analyses on the MNIST dataset.

Mathematical Principle

Max Pooling

Max pooling is the most commonly used operation in modern CNN architectures. Its principle is remarkably simple: for each rectangular region of the input map, only the maximum value is retained.

Formally, for an input map X and an output Y, the operation is expressed as:

y_{i,j} = max_{0≤a≤k, 0≤b≤k} x_{s·i+a, s·j+b}

where:
– k is the kernel size
– s is the stride
– i and j are the spatial indices of the output

For example, with a 2×2 kernel and a stride of 2, each non-overlapping 2×2 window produces a single number: the maximum of the four values contained in that window. The output map’s dimensions are then halved.

This mechanism has an important property: max pooling preserves the strongest activations, which generally correspond to the most discriminant features (sharp edges, characteristic textures, particular patterns). On the other hand, it loses exact position information within each window — which, counterintuitively, contributes to the model’s robustness against slight shifts.

The gradient path during backpropagation is particularly interesting: only the maximum element of each window receives the gradient, while all other elements see their gradient zeroed out. This mechanism is sometimes called a switch network, as the network implicitly learns which neurons are “active” at each stage.

Average Pooling

Average pooling computes the mean of values in each window rather than the maximum:

y_{i,j} = 1/(k²) · Σ_{0≤a≤k, 0≤b≤k} x_{s·i+a, s·j+b}

This operation produces a more “smoothed” representation of the input map. Each element of the output contributes to the final value, unlike max pooling where a single element dominates.

Average pooling is particularly useful when you want to retain an overall view of the region rather than focusing on an activation peak. It is frequently employed at the end of a network, for example in the form of global average pooling, which advantageously replaces fully connected layers (as in the ResNet architecture). By spatially averaging each channel over its entire surface, you obtain a fixed-size vector directly usable for classification, which drastically reduces the total number of parameters.

Adaptive Pooling

Adaptive pooling solves a specific problem: how to obtain a predefined output size regardless of the input dimension?

Unlike classical pooling where you set the kernel size and stride, adaptive pooling dynamically adjusts the regions to produce exactly the desired output. If the input is H×W and you want an output of H’×W’, each output cell (i, j) is computed by averaging (or taking the maximum of) the corresponding region, with bounds computed to cover the entire input proportionally. The regions may therefore have slightly different sizes, but the output always has the exact requested dimension.

This flexibility is valuable in many scenarios: processing images of varying resolutions, architectures requiring variable input sizes, or when chaining modules whose dimensions are not compatible a priori.

Fractional Max Pooling

Fractional max pooling introduces additional regularization by using variable-sized regions, determined randomly during training. Unlike classical max pooling which uses uniform and fixed windows, fractional pooling randomly chooses region boundaries according to a stochastic process.

Formally, given a sequence of randomly generated cutting points, each region can have a different size from the others. This random behavior acts as a regularization mechanism, preventing the model from memorizing patterns that are too specific to exact positions. During inference, the average of several realizations of fractional pooling is generally used, or alternatively a deterministic version with fixed-size regions.

This technique is particularly effective at reducing overfitting in deep architectures, as it forces the network to develop more robust representations that are less sensitive to precise spatial variations.

Stochastic Pooling

Stochastic pooling proposes a radically different approach: instead of systematically selecting the maximum (like max pooling) or averaging all values (like average pooling), it probabilistically samples an element from each window.

More precisely, each value x_{a,b} in a window is normalized to form a probability distribution:

p_{a,b} = x_{a,b} / Σ_{a’,b’} x_{a’,b’}

Then, an element is randomly drawn according to this distribution. Strong activations have a higher chance of being selected, but weak activations are not totally ignored. This approach combines the advantages of max pooling (preservation of important features) and stochastic regularization (prevention of overfitting).

During the inference phase, mathematical expectation is generally used, i.e., the weighted sum according to probabilities, which amounts to a form of weighted average pooling.

Intuition: Looking at a Map at Different Scales

To truly understand the usefulness of pooling layers, imagine consulting a geographic map at different scales.

Up close, you see every street, every building, every precise detail of your neighborhood. This is the high resolution: information is rich, but also very voluminous and potentially noisy. Every small variation counts, making global analysis difficult.

Zooming out, individual streets gradually disappear. But neighborhoods, main arteries, and major avenues remain perfectly visible. You have lost local precision, but you have gained the ability to perceive the overall structure. The essence remains, stripped of the noise.

Pooling works in exactly the same way. At each pooling layer, the network “zooms out” on its own internal representations. It abandons overly fine details to focus on broader structures: instead of remembering “there is a very activated pixel at coordinates (42, 17),” the network remembers “there is a skyscraper somewhere in this city block.”

Max pooling specifically retains the most salient element of each zone — like an observer who immediately spots the tallest building in the neighborhood. Average pooling, on the other hand, gives a more general impression of the density and intensity of activity in the zone.

This property of progressive translation invariance is fundamental: a cat photographed with a few-pixel shift must still be recognized as a cat. Pooling makes the network less sensitive to these minor variations, which considerably improves its ability to generalize.

Implementation with PyTorch

Comparing the Three Types of Pooling

Let’s start with a visual demonstration of the effect of each type of pooling on a synthetic image:

import torch
import torch.nn as nn
import matplotlib.pyplot as plt
import numpy as np

# Creation of a synthetic 8x8 image with interesting patterns
dummy_input = torch.tensor([
    [0.1, 0.2, 0.3, 0.4, 0.1, 0.2, 0.3, 0.4],
    [0.2, 0.8, 0.9, 0.5, 0.2, 0.8, 0.9, 0.5],
    [0.3, 0.9, 1.0, 0.6, 0.3, 0.9, 1.0, 0.6],
    [0.4, 0.5, 0.6, 0.7, 0.4, 0.5, 0.6, 0.7],
    [0.1, 0.2, 0.3, 0.4, 0.0, 0.1, 0.2, 0.3],
    [0.2, 0.9, 0.8, 0.5, 0.1, 0.9, 0.8, 0.5],
    [0.3, 0.7, 0.6, 0.3, 0.2, 0.7, 0.6, 0.3],
    [0.4, 0.5, 0.4, 0.2, 0.3, 0.5, 0.4, 0.2],
], dtype=torch.float32)

# Adding batch and channel dimensions
x = dummy_input.unsqueeze(0).unsqueeze(0)  # (1, 1, 8, 8)

# Max Pooling 2x2 with stride 2
max_pool = nn.MaxPool2d(kernel_size=2, stride=2)
max_output = max_pool(x)

# Average Pooling 2x2 with stride 2
avg_pool = nn.AvgPool2d(kernel_size=2, stride=2)
avg_output = avg_pool(x)

# Adaptive Average Pooling -> output 4x4
adaptive_pool = nn.AdaptiveAvgPool2d((4, 4))
adaptive_output = adaptive_pool(x)

print("Input (8x8):")
print(dummy_input.numpy())
print("\nMax Pooling (4x4):")
print(max_output.squeeze().numpy())
print("\nAverage Pooling (4x4):")
print(avg_output.squeeze().numpy())
print("\nAdaptive Average Pooling (4x4):")
print(adaptive_output.squeeze().numpy())

Analysis of results: Max pooling systematically extracts activation peaks (the highest values in each 2×2 window), while average pooling produces intermediate values, reflecting the average intensity of each region. Adaptive pooling produces a result very similar to average pooling in this case (since 8/4 = 2, the calculation is equivalent), but its strength lies in its ability to handle arbitrary input sizes.

Visualizing the Effect of Each Pooling

def visualize_pooling_comparison(original, max_out, avg_out, adaptive_out):
    fig, axes = plt.subplots(1, 4, figsize=(16, 4))

    im0 = axes[0].imshow(original.numpy(), cmap='hot', vmin=0, vmax=1)
    axes[0].set_title('Original input (8×8)')
    axes[0].axis('off')
    plt.colorbar(im0, ax=axes[0], fraction=0.046)

    im1 = axes[1].imshow(max_out.squeeze().numpy(), cmap='hot', vmin=0, vmax=1)
    axes[1].set_title('Max Pooling (4×4)')
    axes[1].axis('off')
    plt.colorbar(im1, ax=axes[1], fraction=0.046)

    im2 = axes[2].imshow(avg_out.squeeze().numpy(), cmap='hot', vmin=0, vmax=1)
    axes[2].set_title('Average Pooling (4×4)')
    axes[2].axis('off')
    plt.colorbar(im2, ax=axes[2], fraction=0.046)

    im3 = axes[3].imshow(adaptive_out.squeeze().numpy(), cmap='hot', vmin=0, vmax=1)
    axes[3].set_title('Adaptive Pooling (4×4)')
    axes[3].axis('off')
    plt.colorbar(im3, ax=axes[3], fraction=0.046)

    plt.tight_layout()
    plt.savefig('pooling_comparison.png', dpi=150, bbox_inches='tight')
    plt.show()

visualize_pooling_comparison(dummy_input, max_output, avg_output, adaptive_output)

Impact on CNN Performance on MNIST

Now let’s study the real impact of different pooling strategies on the performance of an MNIST classifier:

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# Loading MNIST
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])

train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST('./data', train=False, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False)

class SimpleCNN(nn.Module):
    def __init__(self, pooling_type='max'):
        super().__init__()
        self.pooling_type = pooling_type

        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
        self.bn1 = nn.BatchNorm2d(32)

        if pooling_type == 'max':
            self.pool1 = nn.MaxPool2d(2, 2)
            self.pool2 = nn.MaxPool2d(2, 2)
        elif pooling_type == 'avg':
            self.pool1 = nn.AvgPool2d(2, 2)
            self.pool2 = nn.AvgPool2d(2, 2)
        elif pooling_type == 'fractional':
            self.pool1 = nn.FractionalMaxPool2d(2, output_size=(12, 12))
            self.pool2 = nn.FractionalMaxPool2d(2, output_size=(5, 5))
        elif pooling_type == 'adaptive':
            self.pool1 = nn.AdaptiveAvgPool2d((14, 14))
            self.pool2 = nn.AdaptiveAvgPool2d((5, 5))
        else:  # no pooling
            self.pool1 = nn.Identity()
            self.pool2 = nn.Identity()

        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        self.bn2 = nn.BatchNorm2d(64)
        self.dropout = nn.Dropout2d(0.25)

        # Classifier input size depends on pooling
        if pooling_type == 'none':
            fc_input = 64 * 28 * 28
        else:
            fc_input = 64 * 5 * 5

        self.fc1 = nn.Linear(fc_input, 128)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = self.pool1(F.relu(self.bn1(self.conv1(x))))
        x = self.pool2(F.relu(self.bn2(self.conv2(x))))
        x = self.dropout(x)
        x = x.view(x.size(0), -1)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return F.log_softmax(x, dim=1)

def train_and_evaluate(pooling_type, epochs=5):
    model = SimpleCNN(pooling_type=pooling_type)
    optimizer = optim.Adam(model.parameters(), lr=1e-3)

    for epoch in range(epochs):
        model.train()
        for batch_idx, (data, target) in enumerate(train_loader):
            optimizer.zero_grad()
            output = model(data)
            loss = F.nll_loss(output, target)
            loss.backward()
            optimizer.step()

    model.eval()
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            output = model(data)
            pred = output.argmax(dim=1)
            correct += pred.eq(target).sum().item()

    accuracy = correct / len(test_loader.dataset)
    params = sum(p.numel() for p in model.parameters())
    return accuracy, params

# Evaluation of each strategy
results = {}
for ptype in ['max', 'avg', 'adaptive', 'fractional', 'none']:
    acc, params = train_and_evaluate(ptype, epochs=5)
    results[ptype] = {'accuracy': acc, 'params': params}
    print(f"Type: {ptype:12s} | Accuracy: {acc:.4f} | Parameters: {params}")

Typical results obtained:

Strategy Accuracy Parameters Observations
Max Pooling ~99.1% ~530,000 Classic reference, excellent trade-off
Average Pooling ~98.9% ~530,000 Slightly below, but very stable
Adaptive Pooling ~99.0% ~530,000 Similar to average, more flexible
Fractional Max ~98.7% ~530,000 Increased regularization, useful on small datasets
No pooling ~98.5% ~1,800,000 Many more parameters, risk of overfitting

These results clearly illustrate the value of pooling: it not only improves accuracy through regularization but also considerably reduces the number of parameters, which speeds up training and decreases memory consumption.

Hyperparameters

The judicious choice of pooling hyperparameters is crucial for the final performance of the model. Here are the main parameters to consider:

kernel_size

The kernel size determines the extent of the region over which the pooling operation is applied. The most common values are:

  • 2×2: The industry standard. Halves spatial dimensions at each application while retaining sufficient information.
  • 3×3: Sometimes used for more aggressive reduction, but at the cost of more significant information loss.
  • Global (H×W): Used only at the end of the network for global pooling, producing one scalar per channel.

In practice, a 2×2 kernel offers the best trade-off between dimensionality reduction and information preservation. Larger kernels tend to eliminate too much detail and can harm the model’s ability to learn fine patterns.

stride

The stride controls the overlap between consecutive pooling windows:

  • stride = kernel_size: No overlap — this is the most common and recommended case. Each zone of the input is processed exactly once.
  • stride < kernel_size: Overlap — windows partially overlap. This preserves more information (since each input value can contribute to multiple outputs) but increases the output map size.
  • stride > kernel_size: Some zones of the input are skipped — generally discouraged except in very specific cases.

padding

Padding is rarely used with pooling layers, since the pooling operation does not change the number of channels and padding could introduce artifacts (artificial zeros influencing maxima or averages). However, some frameworks allow it for specific cases.

adaptive_output_size

For adaptive pooling, this parameter directly specifies the desired output size. It is independent of the input size, which is its main advantage:

  • AdaptiveAvgPool2d((7, 7)): Very common before fully connected layers in architectures like ResNet.
  • AdaptiveAvgPool2d((1, 1)): Global pooling, produces exactly one value per channel.

pooling_type

The choice of pooling type directly influences the network’s behavior:

  • Max pooling: To be preferred in the majority of cases, especially for local feature detection.
  • Average pooling: Preferable at the end of the chain or when a more consensual representation is desired.
  • Adaptive pooling: Essential when input sizes vary or when a fixed output size is required.
  • Fractional max pooling: Useful as an alternative regularization technique to dropout.
  • Stochastic pooling: Experimental but promising for intrinsic regularization.

Advantages and Limitations

Advantages

  1. Dimensionality reduction: Pooling progressively reduces the spatial size of feature maps, which reduces the number of parameters in subsequent layers and speeds up computation.
  2. Intrinsic regularization: By abandoning certain details, pooling prevents the network from memorizing noise and peculiarities of the training set. It is a form of overfitting prevention integrated into the architecture itself.
  3. Translation invariance: A detected object slightly shifted will produce the same activation after pooling. This property is essential for the robustness of image classifiers.
  4. Enlarged receptive field: By reducing spatial resolution while preserving channel depth, each neuron in subsequent layers “sees” an increasingly larger region of the original image.
  5. Reduced memory consumption: Smaller feature maps mean less GPU memory needed, which allows training deeper models or using larger batch sizes.

Limitations

  1. Spatial information loss: Pooling inevitably loses details. For tasks requiring precise localization such as semantic segmentation or object detection, this loss can be problematic. This is why modern architectures like U-Net or Mask R-CNN use skip connections to recover this information.
  2. Fixed and not learned: Pooling parameters (size, stride) are static hyperparameters, not learned during training. Unlike convolution weights that adapt to data, pooling always applies the same rule.
  3. Sensitivity to kernel and stride choice: A poor choice can lead to excessive information loss or, conversely, insufficient reduction, unnecessarily increasing computational cost.
  4. Gradient problem: With max pooling, only the maximum element receives the gradient. If a non-maximal element contained useful information, it will never be updated by backpropagation. This is an “all or nothing” form that can limit learning.

4 Concrete Use Cases

1. Image Classification with ResNet

In ResNet architectures, global average pooling replaces traditional fully connected layers. After the last convolutional layer producing feature maps of size H×W×C, an AdaptiveAvgPool2d((1, 1)) reduces this output to a vector of size C. Each element of this vector represents the average activation of a channel over the entire image, forming a compact and discriminant signature for classification.

class ResNetClassifier(nn.Module):
    def __init__(self, num_classes=1000, backbone=None):
        super().__init__()
        self.backbone = backbone  # e.g., pretrained resnet50
        self.backbone.fc = nn.Identity()  # remove original FC
        self.global_pool = nn.AdaptiveAvgPool2d((1, 1))
        self.classifier = nn.Linear(2048, num_classes)

    def forward(self, x):
        features = self.backbone(x)       # (B, 2048, 7, 7)
        pooled = self.global_pool(features)  # (B, 2048, 1, 1)
        pooled = pooled.view(pooled.size(0), -1)  # (B, 2048)
        return self.classifier(pooled)

2. Semantic Segmentation with U-Net

In U-Net, pooling (typically max pooling 2×2) is used in the encoder path to progressively reduce spatial resolution while increasing semantic depth. Each pooling layer halves the feature map size, allowing the network to capture increasingly global features — from simple edges to complete objects.

However, to reconstruct a pixel-by-pixel segmentation, U-Net uses skip connections that reconnect the high-resolution feature maps from the encoder path to the decoder path, thus compensating for the information loss caused by pooling.

3. Transfer Learning with Variable Input

When adapting a pretrained model to a new domain, input images may have different resolutions than those used during initial training. Adaptive pooling allows accepting these variably-sized inputs without modifying the weights of the pretrained model. Simply inserting an adaptive layer before the classifier guarantees a constant-dimension output, regardless of input resolution.

4. Receptive Field Augmented Networks

Some specialized architectures use aggressive pooling (e.g., 3×3 or 4×4 kernels) in the early layers to quickly force a wide receptive field. This design is useful for tasks where global context is more important than local details, such as scene classification or monocular depth estimation from a single image.

See Also


About Salah YAHIAOUI

Salah YAHIAOUI publie sur The Pingouin des tutoriels Python et des décryptages sur l’intelligence artificielle. Les cas pratiques récents précisent leurs données, leurs tests et leurs limites. À propos, méthode éditoriale et contact.

View all posts by Salah YAHIAOUI →