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:
- Channel count is preserved: If the input has C channels, the output has exactly C channels. Pooling never creates, removes, or mixes channels.
- Feature map semantics are preserved: If channel 42 detects horizontal edges, the pooled output of channel 42 still represents horizontal edges, just at half the spatial resolution.
- No cross-channel reasoning: Pooling cannot learn that two channels should be combined. That job belongs to the convolutional layers, which mix channels via their filter weights.
- Independent max indices: The position of the maximum in channel c is unrelated to the position of the maximum in channel c'. Different channels may have their max at different spatial positions within the same 2x2 window.
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:
- After Pool 1: 224 \times 224 \to 112 \times 112 (after conv block 1, 64 channels)
- After Pool 2: 112 \times 112 \to 56 \times 56 (after conv block 2, 128 channels)
- After Pool 3: 56 \times 56 \to 28 \times 28 (after conv block 3, 256 channels)
- After Pool 4: 28 \times 28 \to 14 \times 14 (after conv block 4, 512 channels)
- After Pool 5: 14 \times 14 \to 7 \times 7 (after conv block 5, 512 channels)
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:
- Max pooling: Preserves peak response, ideal for detecting whether a feature is present somewhere in the local region.
- Average pooling: Preserves overall activation density, useful for estimating how broadly a feature is activated across the region.
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
- Reshape height and width into output and window axes.
- Reduce the two window axes with max.
Requirements
- Use NumPy.
- Reduce each non-overlapping 2 by 2 spatial window with maximum.
- Preserve batch and channel axes.
- Return a float64 NumPy array.
Constraints
- x has shape (B, H, W, C) and dtype float64.
- H and W are positive even integers.
- B and C are positive.
Starter Code
import numpy as np
def vgg_maxpool(x: np.ndarray) -> np.ndarray:
"""
Returns the float64 pooled tensor in NHWC layout.
"""
passTest Cases
| Case | Matches | |
|---|---|---|
| Single channel | — | public |
| Two channels | — | public |
| Negative values | — | public |