What a Conv2D layer actually computes: multi-channel cross-correlation
How a Conv2D layer differs from the textbook 2D convolution in scipy: it skips the kernel flip, each filter spans all input channels, and the outputs of many filters are stacked, which explains its parameter count and why pooling adds none.
University lab From my coursework in the MSc in Statistics and Machine Learning at Linköping University, rewritten as a note to review later.
The CNN lab started with classic filters (Gaussian blur, Sobel edges) run through scipy.signal.convolve2d, and then moved on to Keras Conv2D layers. One question asked whether Conv2D does the same standard 2D convolution. I answered yes. It doesn’t, and the differences explain things I got wrong later in the same lab, such as where the parameters come from.
Three differences from convolve2d
1. No kernel flip. Mathematical convolution flips the kernel before sliding it:
Deep learning libraries compute cross-correlation, which slides the kernel as it is:
is the image, the kernel, and the output position. Since the kernel is learned, the flip doesn’t matter: the network would just learn the flipped kernel. But for a fixed kernel like Sobel, the two give different signs.
2. Each filter spans all input channels. A colour image has 3 channels. A filter in the first layer is really . It looks at all three channels at once and sums everything into one output map. That’s why the lab’s GPU test talked about “7 × 7 × 3” filters. In a later layer with 32 input channels, each filter is .
3. Many filters, stacked. A layer with filters produces maps, stacked as channels. Putting it together, output channel at position is
where is input channel , is the slice of filter that looks at channel , and is one bias per filter.
import numpy as np
from scipy.signal import convolve2d, correlate2d
rng = np.random.default_rng(0)
x = rng.normal(size=(8, 8, 3)) # H x W x C_in
w = rng.normal(size=(3, 3, 3, 16)) # K x K x C_in x C_out
b = np.zeros(16)
def conv2d_layer(x, w, b):
"""What a Conv2D layer computes ('valid' padding, stride 1)."""
K, _, C_in, C_out = w.shape
out = np.zeros((x.shape[0] - K + 1, x.shape[1] - K + 1, C_out))
for o in range(C_out):
for c in range(C_in): # sum over ALL input channels
out[:, :, o] += correlate2d(x[:, :, c], w[:, :, c, o], mode="valid")
out[:, :, o] += b[o]
return out
print(conv2d_layer(x, w, b).shape) # (6, 6, 16)
print(w.size + b.size) # 448 = (3*3*3 + 1) * 16
k = w[:, :, 0, 0] # correlation = convolution with a flipped kernel
print(np.allclose(correlate2d(x[:, :, 0], k, "valid"),
convolve2d(x[:, :, 0], k[::-1, ::-1], "valid"))) # True
Counting parameters
From the formula above, a Conv2D layer has
parameters: one block of weights plus one bias for each filter. In our 4-layer network, the first layer has , and the third (32 channels in, 64 out) has , matching model.summary().
One lab question asked why a layer’s parameter count isn’t just “number of filters × coefficients per filter (plus biases)” when you think of each filter as . I answered “because of pooling”. That’s wrong. The missing factor is : each filter has a separate slice for every input channel. Pooling has nothing to do with it.
Pooling has no parameters
Max pooling takes the largest value in each window. Nothing is learned, so its parameter count is 0, which model.summary() shows. Pooling cuts parameters only indirectly: it halves the height and width, so when the conv stack ends in Flatten and a Dense layer, that dense layer gets fewer inputs. In our network the last conv output was numbers, so the dense layer had weights. Without the four poolings it would have been , about 4.9 million.
That Flatten → Dense step is also why the trained network only accepts images. The conv layers would run on any size, but the dense layer expects exactly 384 inputs. In the lab I suggested transfer learning to handle other sizes, which isn’t the fix. The fix is to replace Flatten with global average pooling (or another fixed-size pooling), see spatial pyramid pooling.
“Same” vs. “valid”
With “valid” padding the output shrinks by per layer ( for a Sobel filter in the lab). Stack many layers and the maps keep shrinking, and border pixels take part in fewer outputs. “Same” padding adds zeros on each side so the size stays put. The general size formula is in classic CNN architectures.
Key takeaways
- Conv2D computes cross-correlation (no flip), summed over all input channels, with one output map per filter.
- Parameters: . The factor is the part that’s easy to forget.
- Pooling has no parameters. It only shrinks the maps, which shrinks any dense layer after
Flatten. Flatten+Denseties a CNN to one input size. Global pooling removes that limit.
Resources
- 732A78 Deep Learning, Linköping University (CNN lab).
- Goodfellow, Bengio and Courville, Deep Learning, Chapter 9.
- Dumoulin and Visin, A guide to convolution arithmetic for deep learning, 2016.
- SciPy convolve2d and correlate2d.