CNNs for Images Revisited
Suppose you want a machine to tell a cat from a dog in a photograph. The cat might appear in the top-left corner or the bottom-right. It might be small or large, upright or tilted. A naive fully-connected network flattens the image into a vector. It treats every pixel as an independent feature. So it must relearn "cat ear" separately for every possible location. This is an enormous waste of parameters and data. Convolutional neural networks (CNNs) solve this by baking two assumptions about images directly into the architecture: nearby pixels are more related than distant ones, and a useful pattern detector (an edge, a texture, an eye) should work the same way no matter where it appears. This idea — sharing weights across spatial locations and building features hierarchically from small local patches upward — is why CNNs became, and largely remain, the workhorse of computer vision. They power photo search, medical image diagnosis, autonomous-vehicle perception, face recognition, and the vision backbones inside multimodal AI systems. Even as attention-based architectures have entered vision, CNNs still dominate in resource-constrained and real-time settings. Every modern vision system is built on the same three primitives this article revisits: convolutional layers, pooling, and the notion of a receptive field. The big picture is this: a CNN is a stack of learned local filters, applied with weight sharing. These filters are interleaved with nonlinearities and downsampling. Together they gradually transform raw pixels into representations of increasing abstraction — edges, then textures, then object parts, then whole objects. Each layer's neurons only "see" a limited window of the input (their receptive field). This window grows as depth increases. This lets the network build global understanding from local evidence, without ever needing a fully-connected layer over raw pixels. This article gives a precise, from-scratch account of how convolutional layers and pooling work mechanically. It explains why weight sharing and locality make CNNs both parameter-efficient and statistically sound. It shows how to compute receptive fields and output sizes exactly. It also covers where the classic pitfalls lie when implementing or debugging one. It builds on Convolution and Backpropagation, and sets up the machinery needed for Image Classification with Deep Networks and Fully Convolutional Networks.
- A convolutional layer slides a small learned filter over the image, reusing the same weights at every spatial position — this is called weight sharing.
- Locality + weight sharing cut parameters drastically compared to fully-connected layers, and encode a correct prior: image statistics are (approximately) translation-invariant.
- Pooling (max or average) discards small spatial shifts while keeping the strongest activations, giving local translation invariance and reducing computation.
- The receptive field of a unit is the region of the original image that can influence it; it grows with depth, stride, and dilation.
- Stacking conv + nonlinearity + pooling layers builds a feature hierarchy: edges → textures → parts → objects, learned end-to-end via Backpropagation.
- Output spatial size, receptive field size, and parameter count are all computable in closed form — essential for architecture design and debugging.
A convolutional layer detects the same local pattern everywhere in an image by reusing one small set of weights across all spatial positions, trading a global fully-connected search for a cheap, translation-equivariant local one.
The idea
A convolutional layer applies a small learnable filter (also called a kernel), typically of size 3×3 or 5×5 spatially, across every position of its input, computing a weighted sum of the pixels under the filter plus a bias, followed by a pointwise nonlinearity such as ReLU. Formally, for an input feature map with channels indexed by , and a filter of shape , the output at spatial location for output channel is
Crucially, the same weights are used for every — this is weight sharing, and it is what distinguishes a convolutional layer from a locally-connected but unshared layer. Because the same filter is convolved everywhere, translating the input by some shift translates the output feature map by the same shift (ignoring border effects): this property is called <span class="wiki-link-missing">equivariance</span> to translation, not invariance — the representation shifts, but the detector itself does not need to be relearned per location. A pooling layer (max-pooling or average-pooling) partitions each channel of a feature map into small neighborhoods (commonly 2×2) and replaces each neighborhood with a single summary statistic — the maximum or the mean. Pooling has no learnable parameters. Its purpose is threefold: it reduces spatial resolution (and hence computation and memory in later layers), it introduces local translation invariance (a feature detected slightly shifted still produces the same pooled output), and it lets subsequent layers' receptive fields grow faster relative to depth. The receptive field of a unit in some layer is the set of input pixels that can affect its value through the chain of convolutions and poolings preceding it. Early layers have small receptive fields and detect low-level patterns (oriented edges, color blobs); because receptive fields compose (a 3×3 filter on top of a 3×3-filter feature map effectively spans 5×5 of the original input), deeper layers see larger regions and can represent higher-level, more abstract structures — textures, object parts, whole objects. This is the mechanism behind hierarchical feature learning: the architecture itself enforces a coarse-to-fine, local-to-global processing order, and Backpropagation then learns the actual filter weights that make each stage useful for the task.
How it works
Step 1 — Convolutional layer forward pass. Slide each of filters across the input spatially. For an input of size , filter size , stride , and zero-padding on each side, the output spatial size is:
Step 2 — Nonlinearity. Apply an elementwise activation (ReLU: is standard) to each output value — this is what lets the stacked linear convolutions represent nonlinear functions, exactly as in a standard feedforward network.
Step 3 — Pooling. Apply max- or average-pooling over disjoint or overlapping windows (commonly 2×2, stride 2) to downsample the feature map, typically after one or a few conv+ReLU layers.
Step 4 — Stack and repeat. Chain many (convolution → nonlinearity → \[pooling\]) blocks, increasing the number of channels as spatial resolution shrinks, so total computation per layer stays roughly balanced.
Step 5 — Head. Flatten or globally-pool the final feature map and feed it into fully-connected layers (or, in fully convolutional variants, further 1×1 convolutions) to produce the task output — class scores, a segmentation map, detections, etc. This connects directly to Image Classification with Deep Networks and Fully Convolutional Networks.
Step 6 — Backward pass. Gradients flow back through pooling (routed only to the max location for max-pool, or spread evenly for average-pool) and through convolution (the gradient with respect to the input is itself a convolution with the flipped filter, and the gradient with respect to each filter weight sums contributions from every spatial position it touched — because the same weight was reused everywhere) using Backpropagation. ``` function CONV2D_FORWARD(X, W, b, stride, pad):
# X: (H, W, Cin), W: (Cout, Cin, k, k), b: (Cout) Xp = ZERO_PAD(X, pad) Hp, Wp, Cin = SHAPE(Xp) Cout, _, k, _ = SHAPE(W) Hout = floor((Hp - k) / stride) + 1 Wout = floor((Wp - k) / stride) + 1 Y = ARRAY(Hout, Wout, Cout) for o in 1..Cout: for i in 0..Hout-1: for j in 0..Wout-1: acc = b[o] for c in 1..Cin: for u in 0..k-1: for v in 0..k-1: acc += W[o][c][u][v] Xp[istride+u][jstride+v][c] Y[i][j][o] = acc return Y function MAXPOOL_FORWARD(X, pool, stride): H, W, C = SHAPE(X) Hout = floor((H - pool) / stride) + 1 Wout = floor((W - pool) / stride) + 1 Y = ARRAY(Hout, Wout, C) ARGMAX = ARRAY(Hout, Wout, C) # store winning index for backward pass for c in 1..C: for i in 0..Hout-1: for j in 0..Wout-1: window = X[istride : istride+pool, jstride : jstride+pool, c] Y[i][j][c] = MAX(window) ARGMAX[i][j][c] = ARGMAX_INDEX(window) return Y, ARGMAX function MAXPOOL_BACKWARD(dY, ARGMAX, X_shape, pool, stride): dX = ZEROS(X_shape) Hout, Wout, C = SHAPE(dY) for c in 1..C: for i in 0..Hout-1: for j in 0..Wout-1: (u, v) = ARGMAX[i][j][c] # position of the max within its window dX[istride+u][j*stride+v][c] += dY[i][j][c] return dX function CONV2D_BACKWARD(dY, X, W, stride, pad):
# returns dX, dW, db -- dY has shape of forward output Y dW = ZEROS(SHAPE(W)) db = ZEROS(Cout) dXp = ZEROS(SHAPE(ZERO_PAD(X, pad))) Xp = ZERO_PAD(X, pad) Hout, Wout, Cout = SHAPE(dY) for o in 1..Cout: for i in 0..Hout-1: for j in 0..Wout-1: db[o] += dY[i][j][o] for c in 1..Cin: for u in 0..k-1: for v in 0..k-1: dW[o][c][u][v] += Xp[istride+u][jstride+v][c] dY[i][j][o] dXp[istride+u][jstride+v][c] += W[o][c][u][v] dY[i][j][o] dX = REMOVE_PAD(dXp, pad) return dX, dW, db ```
Complexity & correctness
Time complexity of one convolutional layer. For an output of size , each output element requires multiply-adds, so the total cost is
This is the exact operation count of the naive triple/quadruple-nested loop in the pseudocode above; no hidden asymptotic slack exists — every multiply-add is necessary to the definition of the convolution sum. Compare this to a fully-connected layer mapping the same input to an output: it would cost — larger by a factor of roughly , which for a 224×224 image and a 3×3 filter is a factor of about 5500. This gap is the quantitative justification for why weight sharing matters: the inductive bias that "the same detector is useful everywhere" converts a quadratic-in-image-size cost into a cost that is independent of image size (per output pixel).
Parameter count. A convolutional layer has parameters — independent of and . A fully-connected layer mapping the same input to the same output size would need parameters. This is the core statistical argument for CNNs: fewer parameters means a smaller hypothesis space, which by standard learning-theory bounds (VC-dimension or Rademacher-complexity arguments) implies lower sample complexity for a given generalization gap — CNNs need less training data to reach the same test error than an equivalently-sized fully-connected network, provided the translation-equivariance assumption approximately holds for the data, which it does for natural images.
Space complexity. Storing all intermediate activations for backpropagation costs over all layers — this is the classical memory bottleneck of training deep CNNs, and is why techniques like gradient checkpointing trade recomputation for memory in very deep networks.
Correctness of the backward pass. The claim that is a convolution of with the flipped kernel, and that sums over all , follows directly from the multivariate chain rule applied to the forward formula: since depends on additively across all its uses, and the same weight is reused at every , the total gradient with respect to that weight is a sum over all positions that used it — this is exactly weight sharing acting in reverse during backpropagation, and it is the same reasoning used for any tied-weight computation graph.
Receptive field recursion. If layer has kernel size and stride , the receptive field size of a unit at layer , expressed in input pixels, satisfies the exact recursion
Proof sketch: a unit at layer depends on a window of layer units, each spaced input-pixels apart because of accumulated striding; the receptive field therefore extends the previous one by steps of that accumulated stride. Applying this recursion confirms, for example, that two stacked 3×3 convolutions (stride 1) yield a receptive field of 5×5, and three yield 7×7 — matching the intuition used in the figure above, and explaining why deep stacks of small filters (as in VGG-style networks) can match the receptive field of one large filter while using fewer parameters ( vs. per channel pair) and more nonlinearities.
Tradeoffs. Pooling buys translation invariance and cheaper computation but discards precise spatial information, which hurts tasks needing exact localization (this is precisely the problem Fully Convolutional Networks address by avoiding aggressive pooling near the output). Weight sharing is a strong, usually-correct prior for natural images, but it is not automatically correct for data lacking translation symmetry (e.g., aligned face landmarks or tabular data), where it can underfit by forcing an unhelpful symmetry.
Worked example
Consider a tiny 5×5 grayscale image (one channel) convolved with a single 3×3 edge-detecting filter, stride 1, no padding. Input (a vertical edge: dark left half, bright right half): ``` 0 0 5 5 5 0 0 5 5 5 0 0 5 5 5 0 0 5 5 5 0 0 5 5 5 ``` Filter (a horizontal Sobel-like vertical-edge detector), bias : ``` -1 0 1 -2 0 2 -1 0 1 ``` Output size: , so is 3×3. Compute using the top-left 3×3 window of : ``` 0 0 5 0 0 5 0 0 5 ```
By symmetry every window straddling the edge (columns 0–2, 1–3, or 2–4, any row) gives the same computation pattern, so:
| Output position (i,j) | Window columns | |
|---|---|---|
| (0,0),(1,0),(2,0) | cols 0–2 (0,0,5) | 20 |
| (0,1),(1,1),(2,1) | cols 1–3 (0,5,5) | 20 |
| (0,2),(1,2),(2,2) | cols 2–4 (5,5,5) | 0 |
Full output after ReLU (all values already ≥0 here): ``` 20 20 0 20 20 0 20 20 0 ``` The filter fires strongly (value 20) exactly where the 3×3 window straddles the true edge (columns 1–3), and gives zero where the window is entirely inside the bright region (columns 2–4, which is uniform, so all directional derivatives vanish) — this confirms the filter is correctly detecting the vertical edge location, and because the same 9 weights produced this correct response at three different row positions without being retrained, it directly demonstrates weight sharing giving translation-equivariant detection. Now apply 2×2 max-pooling, stride 2, to this 3×3 output. With no padding only one full 2×2 window fits (top-left):
giving a 1×1 pooled output of value 20 — a compact, position-summarized signal that "an edge was detected in this region," discarding exactly where within the region it was, which is the intended local-invariance effect of pooling.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Off-by-one in output size | Forgetting the or wrong padding/stride formula | Always use and unit-test with a known small case |
| Confusing correlation with convolution | Most frameworks implement cross-correlation (no kernel flip) and call it "convolution" | Know which one your framework does; it only matters for interpreting learned filters, not for training correctness |
| Assuming pooling gives global invariance | Pooling only gives local invariance over its window size | Stack multiple pool layers, or use global average pooling before the classifier head for full spatial invariance |
| Ignoring receptive field size vs. object size | A deep stack with small effective receptive field can't "see" large objects | Compute via the recursion; increase depth, kernel size, stride, or dilation as needed |
| Treating stride and dilation as interchangeable | Both change receptive field but stride also changes output resolution while dilation does not | Use dilated (atrous) convolutions when you need larger receptive field without losing resolution |
| Not accounting for parameter sharing in gradient checks | Manually summing per-position gradients incorrectly, double-counting or under-counting | Verify gradients numerically on a small case; the weight gradient must sum contributions from every spatial use |
Pioneers & history
Origins · 1 minYann LeCun and how this was discovered
LeCun combined weight-shared local convolutions, pooling (subsampling), and end-to-end gradient-based training — using Backpropagation through the entire pipeline — into LeNet, demonstrating that a single differentiable pipeline could learn features directly from pixels rather than relying on hand-engineered ones, decades before this became the dominant paradigm in vision.
Where this matters
CNNs are the backbone of virtually every deployed vision system built before the rise of vision transformers, and remain heavily used today for efficiency reasons. Photo organization and reverse image search use CNN embeddings for similarity. Medical imaging systems use CNNs for tumor detection and segmentation in radiology scans. Autonomous vehicles use convolutional backbones for real-time object detection and lane segmentation, often with architectures directly descended from Fully Convolutional Networks. Face recognition and verification systems on phones run small, efficient convolutional networks on-device. Optical character recognition, industrial defect inspection, satellite image analysis, and video action recognition all rely on the same core primitives — local weight-shared filters, pooling, and hierarchical depth — described here, and the same reasoning about receptive fields and parameter efficiency underlies architecture design in Image Classification with Deep Networks and beyond.
Ask the tutor: How do dilated (atrous) convolutions change the receptive-field recursion, and why do they help in dense prediction tasks like segmentation?
Why do CNNs generalize better than fully-connected networks on images?
CNNs encode two correct assumptions about natural images directly into the architecture: locality (nearby pixels are statistically related) and translation equivariance (a useful pattern detector, like an edge or eye, should behave the same wherever it appears). This shrinks the hypothesis space to functions consistent with those assumptions, dramatically reducing the number of parameters and, by standard learning-theory arguments, the amount of training data needed to reach a given generalization error. A fully-connected layer must learn a separate weight for every pixel-to-output connection, effectively re-learning "cat ear" detectors from scratch at every location, which wastes both parameters and data unless the training set already covers every position.
What exactly is a receptive field, and why does it matter?
The receptive field of a unit is the region of the original input image whose pixels can influence that unit's value, traced back through every convolution and pooling operation preceding it. It matters because a unit cannot represent or respond to visual context larger than its receptive field — if you need a network to recognize a large object or scene-level context, but the final layer's receptive field only covers a small patch, the network architecturally cannot succeed regardless of training. Receptive field size is computed via a closed-form recursion involving each layer's kernel size and the accumulated stride of all preceding layers, making it a first-class design quantity rather than something to guess.
Is max-pooling always better than average-pooling?
Neither is universally better; they encode different assumptions. Max-pooling keeps the strongest activation in a window, which works well when the presence of a distinctive feature (a strong edge or texture response) matters more than the average signal, and it tends to produce sparser, sharper feature maps. Average-pooling smooths the signal and can preserve more information about the overall magnitude across a region, which is useful in later or global-pooling stages where you want a holistic summary (as in global average pooling before a classification head). In practice, max-pooling has historically dominated for early/mid layers in classification CNNs, while average-pooling is common as a final global-summarization step.
How does backpropagation through a convolutional layer differ from a fully-connected layer?
The chain rule computation is identical in spirit, but because the same weight is reused at many spatial positions, the gradient with respect to each weight must sum contributions from every position where it was used — this sum arises directly from the multivariate chain rule applied to a function where one variable (the weight) appears additively in many terms. The gradient with respect to the input turns out to be expressible as a convolution of the upstream gradient with the spatially-flipped filter. In a fully-connected layer, each weight is used exactly once, so no such summation over reuse is needed; the shared-weight sum is the only structural difference, and it is what makes convolutional gradients cheaper to compute per parameter but require careful accumulation.
Why does the receptive field grow faster when pooling or strided convolutions are used?
Pooling and strided convolutions increase the accumulated stride — the spacing, in original-input pixels, between adjacent units in a later feature map. Since the receptive-field recursion adds times the accumulated stride at each layer, any operation that increases accumulated stride causes each subsequent layer's kernel to effectively cover proportionally more input pixels per unit of depth. This is why architectures interleave pooling or strided convolutions between plain stride-1 convolutions: it lets receptive fields grow rapidly with depth without requiring impractically large kernels, keeping parameter counts and computation manageable while still reaching global context in a reasonable number of layers.
Can a CNN handle rotation or scale invariance the same way it handles translation?
Not natively. Ordinary convolution is equivariant specifically to translation because sliding the same filter over the image commutes with shifting the image, but rotating or rescaling the input does not commute with a fixed-orientation, fixed-size filter in the same way. Standard CNNs approximate some robustness to small rotations and scale changes through data augmentation (training on rotated/scaled copies) and through pooling's local invariance, but they do not have a built-in mathematical guarantee analogous to translation equivariance. Specialized architectures (steerable filters, group-equivariant CNNs) exist specifically to extend the same weight-sharing principle to rotation and other symmetry groups.
How do I choose kernel size, stride, and depth when designing a CNN?
Start from the receptive field your task needs: object-level classification typically needs a receptive field close to the full image by the final layers, while pixel-level tasks like segmentation need careful control so early layers keep high resolution. Small kernels (3×3) stacked deeply are generally preferred over large kernels because they reach the same receptive field with fewer parameters and more nonlinearities, per the same reasoning used for VGG-style designs. Strides and pooling should be used deliberately, since they trade spatial resolution for receptive-field growth and computational savings, and over-aggressive downsampling early in the network can destroy information needed later, a concern directly relevant to Fully Convolutional Networks.