EasyVGG

VGG Max Pooling

Very Deep Convolutional Networks

Easy

Problem

Implement VGG max pooling on an NHWC tensor using non-overlapping 2 by 2 windows and stride two.

Y_{b,i,j,c} = \max_{r,s\in\{0,1\}} X_{b,2i+r,2j+s,c}

Here, b indexes the batch, i and j index output position, and c indexes channels. Return Y as a float64 NumPy array with shape (B,H/2,W/2,C).

Theory

Max pooling is a spatial downsampling operation that reduces feature map dimensions by selecting the maximum value within each local window. In VGGNet (Simonyan and Zisserman, 2014), max pooling with a 2x2 window and stride 2 is applied after each convolutional block, systematically halving spatial dimensions while preserving channel depth and the strongest activations learned by preceding convolutional filters.


What It Is

Max pooling is a fixed, parameter-free operation that slides a small window across a feature map and outputs only the maximum value found within each window position. It serves as the spatial reduction mechanism between convolutional blocks in VGGNet, compressing height and width while leaving batch size and channel count untouched.

The operation takes a 4D input tensor in NHWC format (batch, height, width, channels) and produces an output with the same batch size N and channel count C, but with halved height and width. Every channel is pooled independently. Unlike convolutional layers, max pooling introduces no learnable parameters. Its behavior is entirely determined by the window size k and the stride s, both fixed at 2 throughout VGGNet.


Key Equations

The Max Pooling Operation

For an input feature map X with shape (N, H_{in}, W_{in}, C) in NHWC format, the output at batch index n, spatial position (i, j), and channel c is:

\text{out}[n, i, j, c] = \max_{0 \leq m < k, \; 0 \leq p < k} X[n, \; i \cdot s + m, \; j \cdot s + p, \; c]

With VGGNet's fixed k = 2 and s = 2, this simplifies to selecting the maximum of exactly 4 elements:

\text{out}[n, i, j, c] = \max\!\big(X[n, 2i, 2j, c], \; X[n, 2i, 2j+1, c], \; X[n, 2i+1, 2j, c], \; X[n, 2i+1, 2j+1, c]\big)

Each output element is the result of comparing exactly four input values arranged in a 2x2 spatial block.

Output Dimensions

The general output dimension formula for pooling with kernel size k, stride s, and no padding is:

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

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

For VGGNet's k = 2, s = 2 with even input dimensions, this reduces cleanly to:

H_{out} = \frac{H_{in}}{2}, \quad W_{out} = \frac{W_{in}}{2}

The full output shape is (N, H_{in}/2, W_{in}/2, C). Batch size and channel count pass through unchanged.


Why Max Pooling

Max pooling serves three distinct purposes in convolutional architectures, all of which are essential to VGGNet's design.

Spatial Downsampling

Each pooling layer halves both height and width, reducing the total number of spatial positions by a factor of 4. This progressive compression allows the network to build increasingly abstract representations: early layers capture fine textures at high resolution, while deeper layers capture object-level semantics at coarser resolution. Without pooling, VGGNet would need to maintain full 224 \times 224 resolution through all layers, making the network computationally intractable.

Translation Invariance

Max pooling provides a degree of translation invariance within each pooling window. If a feature shifts by one pixel within a 2x2 region, the maximum value often remains the same, producing an identical pooled output. Small spatial shifts in the input do not change the network's response, which is desirable for classification where the exact position of a feature matters less than its presence. This invariance compounds across VGGNet's five pooling stages.

Reducing Computation

Convolution cost scales directly with the number of spatial positions. Halving H and W via pooling reduces this cost by 4\times for all subsequent layers. VGGNet's five pooling layers collectively reduce spatial dimensions from 224 \times 224 = 50{,}176 positions down to 7 \times 7 = 49 positions, a compression factor exceeding 1000x. Without this reduction, the computational cost of deeper convolutional layers would be enormous.


The 2x2 Window

VGGNet's choice of a 2x2 pooling window with stride 2 is deliberate and differs from the 3x3/stride-2 overlapping pooling used in AlexNet.

Four Elements, Pick the Max

At each output position, the pooling window covers exactly four input elements arranged in a 2x2 square. The operation selects the single largest value among these four. The other three values are discarded. This is a lossy compression: from four values, only one survives. During backpropagation, the gradient flows only to the position that held the maximum value, and the three non-max positions receive zero gradient.

Non-Overlapping with Stride 2

Because s = k = 2, adjacent pooling windows do not overlap. Each input element belongs to exactly one pooling window. The feature map is partitioned into a grid of non-overlapping 2x2 blocks, with no pixel shared between windows and no pixel left uncovered (assuming even dimensions).

This contrasts with AlexNet's overlapping pooling (k=3, s=2), where each 3x3 window shares one row or column with its neighbors. The VGGNet authors found overlapping pooling unnecessary given their deep stacks of small 3x3 convolutions, which already provide sufficient regularization through depth.


Channel Independence

Max pooling operates entirely within individual channels. The max operation at position (i, j) for channel c considers only the four values at that spatial location in channel c, never looking at values from channel c' where c' \neq c. This has several important implications:

This per-channel independence is why the pooling formula includes the channel index c on both sides of the equation without any summation or interaction term across channels.


Paper Context

Simonyan and Zisserman (2014) describe their pooling configuration concisely: "Max-pooling is performed over a 2x2 pixel window, with stride 2." This single sentence defines the pooling layer used throughout all VGGNet configurations (VGG-11 through VGG-19).

Five Max Pool Layers

VGGNet organizes its convolutional layers into five blocks, with one max pooling layer at the end of each block. The number of conv layers per block varies by configuration (VGG-16 uses 2-2-3-3-3, VGG-19 uses 2-2-4-4-4), but every configuration has exactly five pooling layers.

Spatial Halving Cascade

Starting from the standard ImageNet input of 224 \times 224:

Each pooling layer exactly halves both spatial dimensions because k = s = 2 and all dimensions are even at every stage. The total spatial reduction is 224/7 = 32 = 2^5, corresponding to the five pooling layers.

Channel count doubles at each block boundary (64, 128, 256, 512, 512), while spatial dimensions halve. This creates a roughly constant computational budget per block: halving spatial dimensions reduces FLOPs by 4x, while doubling channels increases FLOPs by roughly 4x.

Design Philosophy

The VGGNet paper's central insight was that deep networks with small 3x3 filters outperform shallower networks with larger filters. Max pooling provides the necessary spatial reduction between blocks, allowing the network to grow deeper without exploding in computational cost. The pooling layers are not the paper's contribution, but they are essential infrastructure that makes the deep 3x3 architecture viable.


Numerical Example

Consider a single-channel 4x4 feature map processed by a 2x2 max pool with stride 2.

Input Feature Map (4x4)

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

Output Dimensions

H_{out} = \frac{4}{2} = 2, \quad W_{out} = \frac{4}{2} = 2

The 4x4 input produces a 2x2 output. Four non-overlapping 2x2 windows tile the input exactly.

Window-by-Window Computation

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

W_{0,0} = \begin{bmatrix} 1 & 3 \\ 5 & 6 \end{bmatrix}

\max(1, 3, 5, 6) = 6

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

W_{0,1} = \begin{bmatrix} 2 & 8 \\ 1 & 4 \end{bmatrix}

\max(2, 8, 1, 4) = 8

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

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

\max(7, 2, 3, 4) = 7

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

W_{1,1} = \begin{bmatrix} 9 & 0 \\ 6 & 5 \end{bmatrix}

\max(9, 0, 6, 5) = 9

Output Feature Map (2x2)

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

The output retains the dominant activation from each local region. The original 16 values have been compressed to 4. Because s = k = 2, no input element appears in more than one window: rows 0-1 feed the top output row, rows 2-3 feed the bottom, columns 0-1 feed the left output column, columns 2-3 feed the right.


Max vs Average Pooling

The choice between max pooling and average pooling has meaningful consequences for what information is preserved during downsampling.

Max Pooling Preserves the Strongest Activation

Max pooling selects the single largest value from each window. After ReLU activation (which VGGNet applies after every convolutional layer), the strongest activation indicates the most confident feature detection. If a convolution filter fires strongly at one position within a 2x2 window, max pooling preserves that response regardless of what the other three positions contain. This makes max pooling well-suited for classification where the question is "is this feature present?" rather than "how much is present on average?"

Average Pooling Smooths

Average pooling computes the arithmetic mean of all values in the window, preserving overall magnitude but diluting strong signals. After ReLU, many activations are zero, and averaging with zeros pulls the pooled value down. A window containing [0, 0, 0, 8] produces \max = 8 but \text{avg} = 2, weakening the detection signal by a factor of 4.

VGG Chose Max Pooling

The VGGNet authors followed the convention established by AlexNet in using max pooling for spatial reduction. Max pooling was the dominant choice for classification architectures of that era because it consistently yielded better accuracy than average pooling in hidden layers. The fundamental tradeoff:


Pitfalls

Using the Wrong Stride

VGGNet pooling requires both k = 2 and s = 2. A common mistake is setting stride to 1 while keeping $ = 2$, which produces overlapping pooling and outputs with dimensions H_{in} - 1 instead of H_{in} / 2. For a 112 \times 112 input, stride 1 produces $ \times 111$ instead of the correct 56 \times 56. This cascading error causes shape mismatches in every subsequent layer.

Non-Divisible Dimensions

The formula H_{out} = H_{in} / 2 only holds when H_{in} is even. If the input has odd dimensions, the floor operation applies:

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

The rightmost column and bottom row are not covered by any window and are silently dropped. In VGGNet, this never occurs because all spatial dimensions in the pipeline are even (224, 112, 56, 28, 14), but when applying VGG-style pooling to custom inputs, odd dimensions cause unexpected size reductions.

Pooling Across Channels

Max pooling operates per channel independently. A common implementation error is to take the max across both spatial and channel dimensions, collapsing multiple channels into one. If the input has shape (1, 4, 4, 64), the correct output is (1, 2, 2, 64). Pooling across channels would produce (1, 2, 2, 1), destroying the per-channel feature structure. In NHWC format, the max operation must be applied only over the spatial window, never over the channel axis.

Confusing Pooling with Strided Convolution

Both 2x2 max pooling with stride 2 and a convolution with stride 2 reduce spatial dimensions by half, but they are fundamentally different. Max pooling has no parameters and selects the maximum with fixed behavior. Strided convolution has learnable weights and computes a weighted sum. In VGGNet, all spatial reduction is performed by max pooling. Every convolutional layer uses stride 1 to preserve spatial dimensions within a block, and reduction happens exclusively at pooling layers between blocks.


Examples

Example 1

Input
x = [[[[1],[3],[2],[4]],[[5],[6],[7],[8]],[[9],[10],[11],[12]],[[13],[14],[15],[16]]]]
Output
[[[[6],[8]],[[14],[16]]]]
Explanation
Each output value is the maximum from its corresponding 2 by 2 spatial window.

Example 2

Input
x = [[[[1,8],[3,2]],[[5,4],[0,7]]]]
Output
[[[[5,8]]]]

Example 3

Input
x = [[[[-1],[-3]],[[-2],[-4]]],[[[-5],[-2]],[[-7],[-6]]]]
Output
[[[[-1]]],[[[-2]]]]

Hints

  1. Reshape height and width into output and window axes.
  2. Reduce the two window axes with max.

Requirements

Constraints

Starter Code

import numpy as np

def vgg_maxpool(x: np.ndarray) -> np.ndarray:
    """
    Returns the float64 pooled tensor in NHWC layout.
    """
    pass

Test Cases

CaseMatches
Single channelpublic
Two channelspublic
Negative valuespublic