Basic CNN Operations
Convolutions slide small weight templates across an image to spot local patterns, while strides, padding, and pooling govern how feature maps shrink.
Why Does This Exist?
A dense layer treats an image as a flat list of independent numbers, and that breaks twice. First on size: feeding a color image with 3 channels into one dense layer of units needs weights, about 12.8 gigabytes in single precision for a single layer. Second on position: shift an edge five pixels right and the dense layer sees a brand-new input it must relearn from scratch.
Convolutions fix both with two constraints: each neuron looks only at a small local patch, and every location reuses the same weights. Stride, padding, and pooling then give exact control over how fast the spatial map shrinks and how fast the receptive field grows.
This page covers the elementwise arithmetic: the sliding sum, the output-size rule, and pooling. The sliding-window picture itself lives in convolutional layers; what each channel of the result means lives in feature maps. Size conventions in isolation are in padding and stride.
Think of It Like This
One rubber stamp swept across a mosaic wall
You must grade every patch of a huge mosaic for one pattern, say a vertical edge. Instead of hiring a separate judge per patch, you carve one small rubber stamp and press it everywhere, writing one score per position. The same stamp finds the edge in the top-left corner and the bottom-right corner, because the stamp never changes, only its position does.
Padding is a blank foam border pinned around the wall so the stamp can center on edge tiles. Stride is how many tiles you hop between presses. Pooling is replacing each group of four scores with just the loudest one so the notebook stays small.
The analogy stops at learning: a rubber stamp is carved once by hand, while a convolution kernel is carved by gradient descent, reshaped a little every training step.
How It Actually Works
Frameworks implement cross-correlation (no kernel flip), though everyone calls it convolution. For an input with channels and spatial size , a filter bank with filters of size , bias (one value per output channel), stride , and zero-padding , the output at channel and position is:
In words: center the kernel at each output position, multiply overlapping pairs, add everything plus the bias. Out-of-bounds reads hit the zero padding.
The output-size rule
The spatial dimensions follow floor arithmetic exactly:
Three settings cover nearly all practice with square kernels (, ):
- Valid padding (): no border added, so with each side shrinks by .
- Same padding (, odd , ): output size equals input size.
- Downsampling stride (): roughly halves each side while the kernel still overlaps fully.
Pooling slides a parameter-free window over the map. Max pooling keeps over each window, preserving the strongest response and tolerating shifts up to the window size. Average pooling keeps the mean, smoothing responses; applied over the whole map it becomes global average pooling, the usual bridge to a classifier head. Modern nets often swap pooling for stride-2 convolution so the downsampler itself can learn.
Worked example
Trace a single-channel input with a vertical-edge kernel , stride , padding , bias :
Output shape first: , so is . The kernel reads left column minus right column at each stop:
- Top-left patch : .
- Top-right patch : .
- Bottom-left patch : .
- Bottom-right patch : .
Positive values mark where the left side is brighter, the vertical-edge signature. A max pool with stride over returns , one number summarizing the whole map.
Code
The snippet below replays the worked example in PyTorch and confirms the hand-computed map plus the pooled value:
import torchimport torch.nn as nn
x = torch.tensor([[[[1.0, 2.0, 0.0, 1.0], [0.0, 3.0, 1.0, 2.0], [2.0, 1.0, 0.0, 2.0], [1.0, 0.0, 3.0, 1.0]]]])
conv = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, stride=1, padding=0, bias=False)weight = torch.tensor([[[[1.0, 0.0, -1.0], [1.0, 0.0, -1.0], [1.0, 0.0, -1.0]]]])with torch.no_grad(): conv.weight.copy_(weight)
out_conv = conv(x)print(out_conv.shape)# -> torch.Size([1, 1, 2, 2])print(out_conv.squeeze().tolist())# -> [[2.0, 1.0], [-1.0, -1.0]]
out_pool = nn.MaxPool2d(kernel_size=2, stride=2)(out_conv)print(out_pool.squeeze().item())# -> 2.0Watch Out For
Dropping the floor in the output-size formula
symptom: a reshape or concatenation crashes several layers downstream of the layer you mis-sized, so the traceback points nowhere near the real mistake. The cause is computing as a float: with , , , the true size is , but the un-floored looks identical and lulls you into skipping the floor where it matters, for example giving , not .
Fix it by always applying integer floor division in size arithmetic and asserting the shape of every feature map once, right after building the model.
Even kernels under symmetric same padding
An even kernel such as or has no single center pixel, so is fractional and symmetric padding cannot preserve size exactly: padding on a kernel over a map yields , shifting every receptive-field center half a pixel. That silent shift breaks weight imports across frameworks and misaligns feature-pyramid concatenations.
Prefer odd kernels (, , ) so is an integer and same padding truly preserves alignment.
The Quick Version
- Convolution slides one shared kernel over the image, multiplying overlapping pairs and summing, which buys local detail plus position reuse.
- Output size obeys per side; never skip the floor.
- Same padding with odd kernels preserves size; stride halves it while the weights still learn.
- Max pooling keeps the strongest response per window; average pooling smooths, and global average pooling feeds classifier heads.
- The worked input through a edge kernel gives a map reading , pooled to .
- Even kernels shift receptive-field centers under symmetric padding, so default to odd sizes.