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.

Key takeaways
  • 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.
The core insight

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 XX with channels indexed by cc, and a filter WW of shape (Cin,k,k)(C_{in}, k, k), the output at spatial location (i,j)(i,j) for output channel oo is

Yo(i,j)=σ ⁣(bo+∑c=1Cin∑u=0k−1∑v=0k−1Wo(c,u,v) Xc(i ⁣+ ⁣u, j ⁣+ ⁣v)).Y_o(i,j) = \sigma\!\left(b_o + \sum_{c=1}^{C_{in}} \sum_{u=0}^{k-1} \sum_{v=0}^{k-1} W_o(c,u,v)\, X_c(i\!+\!u,\, j\!+\!v)\right).

Crucially, the same weights WoW_o are used for every (i,j)(i,j) — 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 CoutC_{out} filters across the input spatially. For an input of size H×W×CinH \times W \times C_{in}, filter size k×kk \times k, stride ss, and zero-padding pp on each side, the output spatial size is:

Hout=⌊H+2p−ks⌋+1,Wout=⌊W+2p−ks⌋+1.H_{out} = \left\lfloor \frac{H + 2p - k}{s} \right\rfloor + 1, \qquad W_{out} = \left\lfloor \frac{W + 2p - k}{s} \right\rfloor + 1.

Step 2 — Nonlinearity. Apply an elementwise activation (ReLU: max⁡(0,x)\max(0,x) 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 CoutC_{out} 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 ```

Figure
A CNN feature hierarchy: receptive fields grow with depth
Layer 1 (edges) Layer 2 (textures) Layer 3 (parts) Head Input 32×32×3 3×3 receptive field Conv 3×3, 16ch RF: 5×5 Pool 2×2 + Conv 3×3 RF: 14×14 Conv 3×3, 64ch RF: 28×28 (whole object) FC → classes

Complexity & correctness

Time complexity of one convolutional layer. For an output of size Hout×Wout×CoutH_{out} \times W_{out} \times C_{out}, each output element requires Cin⋅k⋅kC_{in} \cdot k \cdot k multiply-adds, so the total cost is

O(Hout Wout Cout Cin k2).O(H_{out}\, W_{out}\, C_{out}\, C_{in}\, k^2).

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 H×W×CinH\times W\times C_{in} input to an Hout×Wout×CoutH_{out}\times W_{out}\times C_{out} output: it would cost O(H W Cin⋅Hout Wout Cout)O(H\,W\,C_{in}\cdot H_{out}\,W_{out}\,C_{out}) — larger by a factor of roughly HWk2\frac{HW}{k^2}, 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 Cout (Cink2+1)C_{out}\,(C_{in} k^2 + 1) parameters — independent of HH and WW. A fully-connected layer mapping the same input to the same output size would need H W Cin⋅Hout Wout CoutH\,W\,C_{in}\cdot H_{out}\,W_{out}\,C_{out} 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 O ⁣(∑ℓHℓWℓCℓ)O\!\left(\sum_\ell H_\ell W_\ell C_\ell\right) over all layers ℓ\ell — 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 ∂L/∂X\partial L/\partial X is a convolution of ∂L/∂Y\partial L/\partial Y with the flipped kernel, and that ∂L/∂Wo(c,u,v)\partial L/\partial W_o(c,u,v) sums Xc(i+u,j+v)⋅∂L/∂Yo(i,j)X_c(i+u,j+v)\cdot \partial L/\partial Y_o(i,j) over all (i,j)(i,j), follows directly from the multivariate chain rule applied to the forward formula: since Yo(i,j)Y_o(i,j) depends on Wo(c,u,v)W_o(c,u,v) additively across all its uses, and the same weight is reused at every (i,j)(i,j), 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 ℓ\ell has kernel size kℓk_\ell and stride sℓs_\ell, the receptive field size rℓr_\ell of a unit at layer ℓ\ell, expressed in input pixels, satisfies the exact recursion

rℓ=rℓ−1+(kℓ−1)∏i<ℓsi,r0=1.r_\ell = r_{\ell-1} + (k_\ell - 1)\prod_{i<\ell} s_i, \qquad r_0 = 1.

Proof sketch: a unit at layer ℓ\ell depends on a kℓ×kℓk_\ell \times k_\ell window of layer ℓ−1\ell-1 units, each spaced ∏i<ℓsi\prod_{i<\ell} s_i input-pixels apart because of accumulated striding; the receptive field therefore extends the previous one by (kℓ−1)(k_\ell - 1) 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 (3×(32)=273\times(3^2)=27 vs. 72=497^2=49 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 XX (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 WW (a horizontal Sobel-like vertical-edge detector), bias b=0b=0: ``` -1 0 1 -2 0 2 -1 0 1 ``` Output size: Hout=⌊(5−3)/1⌋+1=3H_{out} = \lfloor(5-3)/1\rfloor+1 = 3, so YY is 3×3. Compute Y(0,0)Y(0,0) using the top-left 3×3 window of XX: ``` 0 0 5 0 0 5 0 0 5 ```

Y(0,0)=(−1)(0)+(0)(0)+(1)(5)+(−2)(0)+(0)(0)+(2)(5)+(−1)(0)+(0)(0)+(1)(5)=5+10+5=20.Y(0,0) = (-1)(0)+(0)(0)+(1)(5) + (-2)(0)+(0)(0)+(2)(5) + (-1)(0)+(0)(0)+(1)(5) = 5+10+5 = 20.

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 columnsY(i,j)Y(i,j)
(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 YY 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):

Pool(0,0)=max⁡(20,20,20,20)=20,\text{Pool}(0,0) = \max(20,20,20,20) = 20,

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

PitfallWhy it happensFix
Off-by-one in output sizeForgetting the +1+1 or wrong padding/stride formulaAlways use ⌊(H+2p−k)/s⌋+1\lfloor (H+2p-k)/s\rfloor + 1 and unit-test with a known small case
Confusing correlation with convolutionMost 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 invariancePooling only gives local invariance over its window sizeStack multiple pool layers, or use global average pooling before the classifier head for full spatial invariance
Ignoring receptive field size vs. object sizeA deep stack with small effective receptive field can't "see" large objectsCompute rℓr_\ell via the recursion; increase depth, kernel size, stride, or dilation as needed
Treating stride and dilation as interchangeableBoth change receptive field but stride also changes output resolution while dilation does notUse dilated (atrous) convolutions when you need larger receptive field without losing resolution
Not accounting for parameter sharing in gradient checksManually summing per-position gradients incorrectly, double-counting or under-countingVerify 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
YL
Yann LeCun
born 1960 · Bell Labs and NYU, pioneer of convolutional networks and their application to handwritten digit recognition

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.

History
1959
— Hubel and Wiesel describe simple and complex cells with local receptive fields in the visual cortex, inspiring later hierarchical models.
1980
— Fukushima's Neocognitron introduces a hierarchical, locally-connected, shift-tolerant architecture for pattern recognition.
1989
— LeCun applies backpropagation to a convolutional network for handwritten digit recognition (early LeNet).
1998
— LeCun et al. publish the modern LeNet-5 architecture, combining convolution, pooling, and gradient-based learning at scale for its time.
2012
— AlexNet, trained on GPUs on ImageNet, triggers the deep-learning resurgence in computer vision and cements CNNs as the dominant vision architecture.

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 (kℓ−1)(k_\ell - 1) 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.

Before this

Where this leads