from d2l import jax as d2l
from flax import nnx
import jax
from jax import numpy as jnpPooling is a parameter-free downsampling operation: slide a window, replace it with a single summary value (max or mean).
Two reasons to use it:
2×2 pool with stride 2 — halves resolution, the canonical example.
Same sliding-window pattern as a convolution, but the operation is max instead of multiply-and-sum:
2×2 max-pool: each output = max of a 2×2 input window. \max(0, 1, 3, 4) = 4.
Average pooling replaces max with mean. Local max-pooling was the standard downsampler in early CNNs and remains in some stems. Modern networks more often use strided convolutions for learned downsampling and global average pooling in the classification head.
A few lines implement the operation: no kernel, only a reduction over each window. Two modes: max and avg.
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = jnp.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y = Y.at[i, j].set(X[i: i + p_h, j: j + p_w].max())
elif mode == 'avg':
Y = Y.at[i, j].set(X[i: i + p_h, j: j + p_w].mean())
return YMax-pooling returns 4, 5, 7, 8, matching the diagram:
Array([[4., 5.],
[7., 8.]], dtype=float32)
A 2×2 max-pool window on [0, 1, 3, 4] returns 4. Shift the input by a pixel; window now sees [1, 0, 4, 0] — still 4.
A small shift moves which element fires, not whether some element in the window fires. As long as the feature stays inside the window, the output is unchanged.
Modern alternative: a strided convolution does the same downsampling but learns its own “pool” function.
Pooling uses the same padding and stride controls as convolution, but a framework MaxPool2d typically matches the stride to the window size to create non-overlapping windows and reduce resolution.
Array([[[[ 0.],
[ 1.],
[ 2.],
[ 3.]],
[[ 4.],
...
[11.]],
[[12.],
[13.],
[14.],
[15.]]]], dtype=float32)
Override the defaults when you want overlapping pools:
Array([[[[ 5.],
[ 7.]],
[[13.],
[15.]]]], dtype=float32)
Convs combine channels (input channels feed every output channel). Pooling does not:
Array([[[[ 0., 1.],
[ 1., 2.],
[ 2., 3.],
[ 3., 4.]],
[[ 4., 5.],
...
[11., 12.]],
[[12., 13.],
[13., 14.],
[14., 15.],
[15., 16.]]]], dtype=float32)
stride=2) handle most downsampling. One initial max-pool, then strided convs.