EasyAlexNet

Overlapping Max Pooling

ImageNet Classification with Deep Convolutional Neural Networks

Easy

Problem

Implement two-dimensional max pooling for an NHWC tensor. A square window of width K moves by stride s, and each output keeps the largest value independently in every batch item and channel.

Y_{n,i,j,c}=\max_{0\le u,v<K}X_{n,is+u,js+v,c}.

Here, n is the batch index, (i,j) is the output location, and c is the channel. AlexNet used windows larger than their stride, so neighboring windows overlap. Return the pooled values as a float64 NumPy array with shape (B,H_{out},W_{out},C).

Theory

Pooling is a downsampling operation in CNNs that reduces spatial dimensions of feature maps by summarizing local regions into single values. In AlexNet (Krizhevsky, Sutskever, and Hinton, 2012), overlapping max pooling was a deliberate architectural choice that contributed to the network's landmark top-5 error rate of 15.3% on ILSVRC-2012.


What Pooling Does

Pooling slides a fixed-size window across an input feature map and computes a single summary value per position, producing a smaller output that retains the most important information while discarding fine-grained spatial detail.

Key purposes:

Pooling operates independently per channel, so channel count is unchanged. Only spatial dimensions (height and width) are reduced.


Max Pooling vs Average Pooling

Max Pooling

Selects the largest value from each k \times k window:

y_{i,j} = \max_{0 \leq m < k, \; 0 \leq n < k} x_{i+m, \; j+n}

During backpropagation, the gradient flows only to the position that held the maximum; all others receive zero gradient.

Average Pooling

Computes the arithmetic mean of all values within each window:

y_{i,j} = \frac{1}{k^2} \sum_{m=0}^{k-1} \sum_{n=0}^{k-1} x_{i+m, \; j+n}

Distributes gradient equally to all positions during backpropagation.

Why AlexNet Used Max Pooling


Key Equations

Output Dimension Formula

For input size H_{in} \times W_{in}, kernel size k, and stride s with no padding:

H_{out} = \left\lfloor \frac{H_{in} - k}{s} \right\rfloor + 1

W_{out} = \left\lfloor \frac{W_{in} - k}{s} \right\rfloor + 1

AlexNet pooling always uses p = 0. The convolution formula includes a +2p term in the numerator; do not confuse the two.

Max Pooling Operation

For input X with C channels, the output at position (i, j) for channel c:

Y(c, i, j) = \max_{0 \leq m < k, \; 0 \leq n < k} X(c, \; i \cdot s + m, \; j \cdot s + n)

The channel index c is the same in input and output since pooling operates per channel independently.

Full Output Shape

Input (N, C, H_{in}, W_{in}) produces output:

(N, \; C, \; H_{out}, \; W_{out})

Batch size N and channel count C are unchanged. Only spatial dimensions H and W are reduced -- a key distinction from convolutional layers, which can change channel count via number of filters.


Overlapping vs Non-Overlapping Pooling

Non-Overlapping Pooling

When s = k, adjacent windows tile the input without sharing pixels. Common: k = 2, s = 2 (halves dimensions). Each input pixel contributes to exactly one output value.

Overlapping Pooling

When s < k, adjacent windows share pixels. The overlap is k - s pixels along each axis. AlexNet uses k = 3, s = 2, giving 1 pixel of overlap.

1D example with input [0, 1, 2, 3, 4], k=3, s=2:

Position 2 is shared -- this is the overlapping pixel.

Why Overlapping Pooling Helps

Shared pixels create information redundancy, acting as a mild regularizer that produces a smoother representation and makes it harder to memorize exact spatial configurations.

It also provides gentler downsampling: k=3, s=3 turns 27x27 into 9x9, while $ s=2$ yields 13x13, preserving more spatial resolution.


Paper Context and Design Decisions

What the Paper Says

Krizhevsky et al. (2012) state: "We generally observe that models with overlapping pooling find it slightly more difficult to overfit." This was significant because overfitting was a major concern for AlexNet's ~60 million parameters trained on only 1.2 million images.

Measured Error Reduction

Switching from non-overlapping (k=2, s=2) to overlapping (k=3, s=2) reduced top-1 error by 0.4% and top-5 error by 0.3%. These fractions mattered in ILSVRC where top entries were separated by less than 1%.

Where Pooling Is Applied in AlexNet

Pooling is applied selectively, not after every conv layer:

Pooling is omitted after Conv3/Conv4 because spatial dimensions are already small (13x13); further pooling would shrink feature maps too aggressively.

Regularization Role

Overlapping pooling was one of several regularization techniques in AlexNet, alongside data augmentation (random crops, flips, PCA color augmentation) and dropout in FC layers. Each contributed a small but additive improvement.


Where Pooling Fits in the AlexNet Pipeline

Each convolutional block follows: Conv -> ReLU -> (optional LRN) -> (optional Pool). LRN is applied after Conv1 and Conv2; pooling after Conv1, Conv2, and Conv5.

Full Spatial Dimension Trace

All pooling layers use k = 3, s = 2.

Input: 224 \times 224 \times 3

Conv1: 96 filters, 11 \times 11, stride 4, no padding.

H_{out} = \left\lfloor \frac{224 - 11}{4} \right\rfloor + 1 = 54

Some implementations use 227x227 to get exactly 55x55. Following paper convention: 55 \times 55 \times 96.

Pool1: k = 3, s = 2

H_{out} = \left\lfloor \frac{55 - 3}{2} \right\rfloor + 1 = 27

After Pool1: 27 \times 27 \times 96

Conv2: 256 filters, 5 \times 5, stride 1, padding 2. Output: $ \times 27 \times 256$

Pool2: k = 3, s = 2

H_{out} = \left\lfloor \frac{27 - 3}{2} \right\rfloor + 1 = 13

After Pool2: 13 \times 13 \times 256

Conv3: 384 filters, 3 \times 3, stride 1, padding 1. Output: $ \times 13 \times 384$ (no pooling)

Conv4: 384 filters, 3 \times 3, stride 1, padding 1. Output: $ \times 13 \times 384$ (no pooling)

Conv5: 256 filters, 3 \times 3, stride 1, padding 1. Output: $ \times 13 \times 256$

Pool5: k = 3, s = 2

H_{out} = \left\lfloor \frac{13 - 3}{2} \right\rfloor + 1 = 6

After Pool5: 6 \times 6 \times 256

Flattened to 6 \times 6 \times 256 = 9216, feeding FC6 (4096) -> FC7 (4096) -> FC8 (1000 classes) with softmax.


Numerical Example

Overlapping max pooling with k = 3, s = 2 on a 5x5 single-channel feature map.

Input Feature Map (5x5)

X = \begin{bmatrix} 1 & 3 & 2 & 7 & 4 \\ 5 & 6 & 8 & 1 & 3 \\ 2 & 9 & 4 & 6 & 5 \\ 3 & 1 & 7 & 2 & 8 \\ 4 & 5 & 3 & 9 & 1 \end{bmatrix}

Output Dimensions

H_{out} = \left\lfloor \frac{5 - 3}{2} \right\rfloor + 1 = 2

Output is 2 \times 2 with 4 pooling windows.

Window Positions and Their Contents

Window (0, 0): rows 0-2, columns 0-2

W_{0,0} = \begin{bmatrix} 1 & 3 & 2 \\ 5 & 6 & 8 \\ 2 & 9 & 4 \end{bmatrix}

\max(W_{0,0}) = 9

Window (0, 1): rows 0-2, columns 2-4

W_{0,1} = \begin{bmatrix} 2 & 7 & 4 \\ 8 & 1 & 3 \\ 4 & 6 & 5 \end{bmatrix}

\max(W_{0,1}) = 8

Window (1, 0): rows 2-4, columns 0-2

W_{1,0} = \begin{bmatrix} 2 & 9 & 4 \\ 3 & 1 & 7 \\ 4 & 5 & 3 \end{bmatrix}

\max(W_{1,0}) = 9

Window (1, 1): rows 2-4, columns 2-4

W_{1,1} = \begin{bmatrix} 4 & 6 & 5 \\ 7 & 2 & 8 \\ 3 & 9 & 1 \end{bmatrix}

\max(W_{1,1}) = 9

Output Feature Map (2x2)

Y = \begin{bmatrix} 9 & 8 \\ 9 & 9 \end{bmatrix}

Identifying the Overlap Regions

With non-overlapping k = 2, s = 2, no pixel would appear in more than one window.


Pooling in Modern Architectures

Strided Convolutions Replacing Pooling

Springenberg et al. (2015) showed max pooling can be replaced by stride-2 convolutions without accuracy loss. ResNet (He et al., 2015) adopted this. The advantage: downsampling becomes learnable rather than fixed.

Global Average Pooling

Lin et al. (2014) introduced GAP as a replacement for FC layers. It computes the spatial average per channel, outputting a vector of length C from a C \times H \times W feature map. This dramatically reduces parameters and was adopted by GoogLeNet (Szegedy et al., 2015) and ResNet.

Adaptive Pooling in PyTorch

PyTorch's nn.AdaptiveMaxPool2d and nn.AdaptiveAvgPool2d let you specify desired output size rather than kernel/stride. For example, nn.AdaptiveAvgPool2d((1, 1)) implements GAP regardless of input size.

Why Pooling Is Less Common in Modern Designs

Despite these trends, GAP before the final classifier remains common, and max pooling persists in lightweight edge models.


Pitfalls

Confusing the Pooling Output Formula with the Convolution Output Formula

AlexNet pooling uses zero padding. The correct formula is:

H_{out} = \left\lfloor \frac{H_{in} - k}{s} \right\rfloor + 1

Do not add a +2p term as for convolution. This error propagates through the network, causing dimension mismatches.

Forgetting That Pooling Preserves the Channel Count

Pooling operates per channel and does not change channel count. Input 96 \times 27 \times 27 with k=3, s=2 produces 96 \times 13 \times 13. Unlike convolution, where output channels equal number of filters.

Off-by-One Errors with Overlapping Windows

The floor operation and +1 are easy to forget. Example: H_{in}=13, k=3, s=2:

H_{out} = \left\lfloor \frac{13 - 3}{2} \right\rfloor + 1 = 6

Forgetting +1 gives 5 instead of 6. Also, overlap width is $ - s$ pixels (1 pixel for $ s=2$), not 2.

Assuming Pooling Has Learnable Parameters

Max and average pooling have zero learnable parameters. When counting AlexNet's ~60M parameters, pooling contributes zero. Behavior is fixed once k and s are chosen.

Ignoring That Pooling Discards Spatial Information Permanently

Max pooling is not invertible -- the locations and values of non-max elements are permanently lost.


Examples

Example 1

Input
x = [[[[1],[2],[3],[4]],[[5],[6],[7],[8]],[[9],[10],[11],[12]],[[13],[14],[15],[16]]]], kernel_size = 3, stride = 1
Output
[[[[11],[12]],[[15],[16]]]]
Explanation
Each three-by-three window contributes its maximum value, and stride one makes adjacent windows overlap.

Example 2

Input
x = [[[[1,8],[2,7]],[[3,6],[4,5]]]], kernel_size = 2, stride = 1
Output
[[[[4,8]]]]

Example 3

Input
x = [[[[1],[5],[2],[4]],[[3],[0],[7],[6]],[[8],[2],[9],[1]],[[4],[3],[5],[0]]]], kernel_size = 2, stride = 2
Output
[[[[5],[7]],[[8],[9]]]]

Hints

  1. Slice each window with row * stride and column * stride as its top-left corner.
  2. np.max(window, axis=(1, 2)) preserves the batch and channel axes.

Requirements

Constraints

Starter Code

import numpy as np

def max_pool2d(x: np.ndarray, kernel_size: int, stride: int) -> np.ndarray:
    """
    Returns the float64 NHWC max-pooled tensor.
    """
    pass

Test Cases

CaseMatches
Overlapping three-by-three poolingpublic
Two channelspublic
Non-overlapping windowspublic