πŸ“š Study Notes / Home / Neural Nets / Session 9
Session 09 Β· Convolutions

Convolutions β€” Seeing with Neural Networks

Welcome to the start of the Computer Vision part of the course. So far our networks have been plain stacks of fully-connected layers (the MLP from Sessions 2–8). Those are wonderful for tidy rows of numbers, but they fall apart on images. In this session we'll discover why images break an MLP, then build β€” from scratch β€” the single idea that fixed it: the convolution. As always, every topic opens with a tiny "explain like I'm 5" story, then we go deep with real math, code, and diagrams. Take it slow; by the end you'll understand exactly what happens when a network "looks at" a picture.

⏱ 23 min readπŸ“– 5 topics

1 Why not just use an MLP for images?


Explain like I'm 5

Imagine you had to describe a photo of a cat to a friend, but the only thing you're allowed to say is the brightness of every single dot, one by one, in order: "dot 1 is grey, dot 2 is grey, dot 3 is orange…" for a million dots. That's exhausting, and worse β€” if the cat slides one step to the left, every dot changes and your whole description is useless! That's exactly the trouble a plain network has with pictures. We need a smarter way to look that notices shapes, not just dots-in-a-row.

Recall from the earlier sessions that an MLP (multi-layer perceptron) is a stack of fully-connected layers: every neuron in a layer is wired to every value coming in. That's fine for, say, 10 features describing a house. But an image is a grid of numbers, and the moment we flatten that grid into one long list to feed an MLP, three serious problems appear.

An image is a grid of numbers

A grayscale image is a 2-D grid of pixels, each a brightness value (often 0–255). A colour image is three such grids stacked β€” one each for Red, Green, Blue β€” which we call channels. So a colour image has shape height Γ— width Γ— channels. A small phone photo might be 224 Γ— 224 Γ— 3.

Problem 1 β€” parameter explosion

To feed an image into an MLP we must flatten it into one long vector. A 224 Γ— 224 Γ— 3 image flattens to 150,528 numbers. If the first hidden layer has just 1,000 neurons, every one of them connects to all 150,528 inputs:

# Parameters in the FIRST layer alone
inputs  = 224 * 224 * 3      # = 150,528 pixel values
neurons = 1000
weights = inputs * neurons   # = 150,528,000  (~150 million)
biases  = neurons            # = 1,000
total   = weights + biases   # > 150 MILLION weights β€” for ONE layer

Over 150 million parameters before we've done anything useful. That's slow to train, hungry for memory, and almost guaranteed to overfit (memorise the training images instead of learning general patterns β€” see Session 6).

Problem 2 β€” it throws away spatial structure

Flattening destroys geometry. In the grid, two pixels that sit side by side are neighbours; once flattened, that pixel and the one directly below it end up 224 slots apart in the list. The MLP has no idea they were ever close. But in vision, nearby pixels are everything β€” edges, corners, and textures are all local patterns. An MLP starts blind to locality.

Problem 3 β€” it isn't translation-invariant

If the network learned to spot a cat's ear in the top-left corner, an MLP has learned a separate set of weights for that exact location. Move the same ear to the bottom-right and those weights are useless β€” it must learn the "ear detector" all over again for every position. We say the MLP is not translation-invariant: shifting the object around the frame breaks it. Real objects move around, so this is fatal.

Concrete example: the wandering "7"

Train an MLP on handwritten digits that always sit dead-centre, and it does fine. Now show it a "7" shoved into the corner. To the MLP, almost every input value is different, so it may confidently call it a "1" or nonsense β€” even though you instantly see it's still a 7. The shape is the same; only the position changed. The MLP never learned "a 7 is a 7 wherever it is."

The big idea (the fix we're about to build)

What if, instead of one neuron staring at the whole image, we used a small detector that looks at one little patch at a time and then slides across the whole image reusing the same weights everywhere? That detector would be tiny (few parameters), respect locality (it sees neighbours), and find its pattern anywhere (translation-invariant). That detector is a convolution β€” the subject of this entire session.

So are MLPs useless now?

Not at all. We still use fully-connected layers at the end of a vision network to make the final decision. The trick is to let convolutions do the heavy "looking" first, and let an MLP do the small "deciding" last. We'll see exactly this layering in Session 10 on CNN architectures.

Recap An MLP on raw images suffers from parameter explosion (millions of weights in layer one), loses spatial structure (flattening hides which pixels are neighbours), and is not translation-invariant (it must relearn a pattern for every position). Convolutions fix all three with a small, sliding, weight-sharing detector.

2 The convolution operation


Explain like I'm 5

Imagine a little magnifying glass that can only see a 3Γ—3 square at a time. You slide it across a picture, square by square, and at every stop it asks one question: "How much does this little patch look like the thing I'm hunting for?" β€” say, a vertical line. It writes down a score at each stop. When you've covered the whole picture, your scores form a new little map showing where the vertical lines are. That magnifying glass is a filter, and sliding it around is a convolution.

Filters / kernels

A filter (also called a kernel) is a small grid of learnable weights β€” commonly 3Γ—3 or 5Γ—5. Each filter is a pattern detector. One might respond strongly to vertical edges, another to a blob of colour, another to a corner. Crucially, the network learns these weights during training (Session 5's backpropagation) β€” we don't hand-design them.

The operation: slide, multiply, sum

At every position the filter visits, we do a dot product: multiply each filter weight by the pixel it's sitting on, then add all those products into a single number. That number is one output value. Slide the filter over by one position and repeat. The grid of all these output numbers is called a feature map (or activation map): a map of "how strongly was this pattern found here?"

πŸ”
1. Place filter
Lay the kernel over a patch
β†’
βœ–οΈ
2. Multiply
Each weight Γ— the pixel under it
β†’
βž•
3. Sum
Add the products β†’ one number
β†’
➑️
4. Slide
Move over, repeat
β†’
πŸ—ΊοΈ
5. Feature map
All scores form a new grid

The math, written out

For an input image I and a kernel K of size k Γ— k, the output value at row i, column j is the sum of element-wise products over the patch:

# Output at position (i, j) β€” a single dot product
S(i, j) = sum over m, n of:  I(i + m, j + n) * K(m, n)

         where m, n range from 0 .. k-1
"Convolution" vs "cross-correlation" β€” a tiny honesty note

Strictly, true mathematical convolution flips the kernel first. What deep-learning libraries actually compute (and what we show here) is cross-correlation β€” the same thing without the flip. Since the kernel weights are learned anyway, the flip makes no practical difference, so everyone just calls it "convolution." Don't let the terminology trip you up.

A tiny numeric example, fully worked

Let's convolve a 4Γ—4 image with a 3Γ—3 vertical-edge detector (a famous classic kernel). With no padding and a step of 1, a 3Γ—3 filter fits in a 4Γ—4 image in a 2Γ—2 set of positions, so the output is 2Γ—2.

# INPUT image (4x4)            # KERNEL (3x3): vertical-edge detector
  3   0   1   2                 1   0  -1
  5   4   2   1                 1   0  -1
  1   2   3   4                 1   0  -1
  0   1   2   3

# --- Top-left position: overlay kernel on the top-left 3x3 patch ---
patch =  3  0  1        kernel = 1  0 -1
         5  4  2                 1  0 -1
         1  2  3                 1  0 -1

# dot product = (1*3 + 0*0 + -1*1)   = 3 + 0 - 1 = 2
#             + (1*5 + 0*4 + -1*2)   = 5 + 0 - 2 = 3
#             + (1*1 + 0*2 + -1*3)   = 1 + 0 - 3 = -2
# total at (0,0) = 2 + 3 + (-2) = 3

# Sliding the kernel to all 2x2 valid positions gives the feature map:
OUTPUT (2x2) =   3    -2
                 6    -2

Notice the kernel's left column is +1 and right column is -1. So it lights up (a big positive or negative number) wherever the left side of the patch is brighter than the right β€” i.e. wherever there's a vertical brightness change, which is exactly an edge. Flat regions (left β‰ˆ right) give values near zero. The filter is an edge detector, purely from its weights.

Worked example: the same code in Python/NumPy

Here is the entire operation written out by hand so you can see there's no magic β€” just loops, multiplies, and adds:

import numpy as np

image = np.array([
    [3, 0, 1, 2],
    [5, 4, 2, 1],
    [1, 2, 3, 4],
    [0, 1, 2, 3],
])

kernel = np.array([
    [1, 0, -1],
    [1, 0, -1],
    [1, 0, -1],
])

k = kernel.shape[0]                          # 3
out_h = image.shape[0] - k + 1           # 4 - 3 + 1 = 2
out_w = image.shape[1] - k + 1           # 2
out = np.zeros((out_h, out_w))

for i in range(out_h):
    for j in range(out_w):
        patch = image[i:i+k, j:j+k]       # the 3x3 window
        out[i, j] = np.sum(patch * kernel) # multiply + sum = dot product

print(out)
# [[ 3. -2.]
#  [ 6. -2.]]   <-- matches our hand calculation above
Key takeaway

A convolution is just one tiny operation β€” overlay, multiply, sum β€” repeated at every location as the filter slides. The output, a feature map, records where the filter's pattern was found. Stack many filters and you get many feature maps, one per pattern the layer hunts for.

Recap A filter/kernel is a small grid of learned weights. The convolution slides it over the image and takes a dot product at every stop, producing a feature map of match scores. Our 3Γ—3 edge detector lit up on vertical brightness changes β€” a real, working edge detector built from nine numbers.

3 Conv layer hyperparameters


Explain like I'm 5

When you use your sliding magnifying glass, you get to choose a few things: How big is the glass (do you peek at a 3Γ—3 patch or a 5Γ—5 one)? How far do you hop between peeks (every step, or skip some)? And do you put a little cardboard frame around the picture so the glass can still reach the very edges? Those choices are the settings of a convolution, and they decide how big your new score-map turns out.

A convolutional layer has a handful of hyperparameters β€” knobs you set before training (unlike weights, which the network learns). Get comfortable with these four; you'll choose them in every CNN you ever build.

Kernel size

The kernel size is the filter's width and height, e.g. 3Γ—3 or 5Γ—5. Small kernels (3Γ—3) see a tiny area but are cheap and, when stacked, can cover large regions β€” which is why modern networks favour them. Larger kernels see more at once but cost more parameters.

Stride

The stride is how many pixels the filter jumps each step. A stride of 1 slides one pixel at a time (lots of overlap, big detailed output). A stride of 2 hops two pixels at a time, skipping positions β€” this downsamples, roughly halving the output's height and width and cutting computation.

Padding (valid vs same)

Notice in Topic 2 that a 4Γ—4 image gave only a 2Γ—2 output β€” the image shrank, and edge pixels were visited fewer times than central ones. Padding fixes this by adding a border of zeros around the image so the filter can reach the edges:

Padding modeWhat it doesEffect on output size
validNo padding at all β€” only "valid" full overlaps are used.Output shrinks (the 4Γ—4 β†’ 2Γ—2 case).
sameAdd just enough zero-border so the output is the same HΓ—W as the input (for stride 1).Output keeps the input's size.

Depth / number of channels

Two "depth" ideas live here, so let's be precise:

  • Input depth (channels in): each filter must be as deep as its input. For an RGB image (3 channels), a 3Γ—3 filter is really 3 Γ— 3 Γ— 3 and sums across all three colour channels into one feature map.
  • Output depth (channels out): a layer has many filters β€” say 32. Each produces its own feature map, so the layer's output is a stack of 32 maps. That stack's depth is the number of filters.

The output-size formula

This is the formula you'll reach for constantly. For an input of size W (one dimension), kernel size K, padding P (zeros added on each side), and stride S:

# Output side length along one dimension
out = floor( (W - K + 2P) / S ) + 1
Worked example: plug in the numbers

Input 32Γ—32, kernel 5Γ—5, with two common setups:

# Case A: valid padding (P=0), stride 1
out = floor((32 - 5 + 0) / 1) + 1 = 27 + 1 = 28   # β†’ 28x28 (shrank)

# Case B: same padding (P=2), stride 1
out = floor((32 - 5 + 4) / 1) + 1 = 31 + 1 = 32   # β†’ 32x32 (preserved)

# Case C: same kernel, stride 2 (downsampling), P=2
out = floor((32 - 5 + 4) / 2) + 1 = floor(15.5) + 1 = 15 + 1 = 16  # β†’ 16x16

Tip: for a kΓ—k kernel with stride 1, "same" padding is P = (k-1)/2 β€” so 1 for a 3Γ—3 kernel, 2 for a 5Γ—5. That's why odd kernel sizes are popular: the padding comes out symmetric.

Receptive fields

The receptive field of an output value is the region of the original input that influenced it. One 3Γ—3 conv has a 3Γ—3 receptive field. But stack a second 3Γ—3 conv on top and each new value depends on a 3Γ—3 patch of the first map β€” which itself each came from 3Γ—3 of the input β€” so the second layer "sees" a 5Γ—5 region. Stack a third and it sees 7Γ—7.

Why this matters

This is the secret to depth: stacking small kernels grows the receptive field cheaply. Early layers (small receptive field) catch tiny features like edges; deeper layers (large receptive field) combine them into bigger concepts like eyes, then faces. Two 3Γ—3 convs see a 5Γ—5 region using far fewer parameters than one 5Γ—5 conv β€” a key insight we'll see exploited by real architectures in Session 10.

Recap The knobs are kernel size (how big the patch), stride (how far it hops β€” bigger stride downsamples), padding (valid shrinks, same preserves size), and depth (input channels per filter, and number of filters = output channels). Output size is floor((W βˆ’ K + 2P)/S) + 1. Stacking small kernels grows the receptive field, letting deep layers see larger patterns.

4 Local connectivity & weight sharing


Explain like I'm 5

Suppose you want to find every red apple in a giant orchard photo. You could hire a million tiny helpers, each memorising one exact spot β€” wildly wasteful. Or you could train one "apple spotter" and let that single expert walk the whole orchard, using the same trained eyes everywhere. One expert, reused over and over, finds apples wherever they are. Convolutions are that one reused expert β€” and that's why they're so cheap and so clever.

Two design choices make convolutions special, and they're the direct cure for the three MLP problems from Topic 1.

Local connectivity

Local connectivity means each output neuron looks at only a small local patch of the input (the kernel-sized window), not the entire image. This respects spatial structure: the neuron only ever combines pixels that are actually near each other. It directly fixes MLP Problem 2.

Weight sharing

Weight sharing (also called parameter sharing) is the big one: the same filter weights are reused at every position as it slides. The network doesn't learn a separate detector per location β€” it learns one detector and applies it everywhere. This gives translation invariance almost for free (fixes Problem 3) and slashes the parameter count (fixes Problem 1).

Worked example: count the parameters

Take the same 224Γ—224Γ—3 image and compare two layers that each produce a rich representation:

# MLP layer: 1000 fully-connected neurons
mlp_params = (224*224*3) * 1000 + 1000
           = 150,528,000 + 1,000
           = 150,529,000           # ~150 MILLION

# Conv layer: 32 filters, each 3x3 over 3 input channels
conv_params = 32 * (3 * 3 * 3) + 32   # weights per filter = 3*3*3 = 27
            = 32 * 27 + 32
            = 864 + 32
            = 896                   # under 900!

Roughly 900 parameters versus 150 million β€” about 168,000Γ— fewer β€” and the conv layer produces 32 feature maps that scan the entire image. The parameter count of a conv layer doesn't even depend on the image's height and width; it depends only on kernel size and channel counts.

PropertyMLP / fully-connectedConvolution
ConnectivityEvery neuron sees all inputs (global)Each neuron sees a small local patch
WeightsUnique weight per connectionSame filter reused at every position
ParametersExplode with image sizeIndependent of image size
TranslationMust relearn per locationInvariant β€” finds patterns anywhere
Spatial structureLost when flattenedPreserved (operates on the grid)
Key takeaway

Local connectivity + weight sharing are why CNNs are both tiny (few parameters, so easier to train and less prone to overfit) and powerful (a learned feature detector automatically works across the whole image). It's the same assumption a human makes: an edge is an edge no matter where in the picture it appears.

Watch out: invariance has limits

Convolutions are robust to small shifts, but a single conv layer isn't magically invariant to big rotations, scaling, or flips. We help with those using data augmentation (randomly shifting/rotating training images) and pooling. Don't oversell "invariance" β€” it's mostly about translation.

Recap Local connectivity means each neuron sees only a small patch (keeps spatial structure); weight sharing means one filter is reused everywhere (gives translation invariance and collapses parameters from ~150 million to under 900 in our example). That combination is what makes CNNs both efficient and effective.

5 Pooling & a full conv block


Explain like I'm 5

Imagine you have a huge, detailed map and you want a smaller summary. You chop it into little 2Γ—2 tiles and, for each tile, keep just the most important fact β€” say, the tallest building. Now your map is a quarter of the size but still tells you where the big stuff is. That shrinking-while-keeping-the- gist trick is pooling. It makes the picture smaller and cheaper to work with, and it stops the network from fussing over the exact pixel a feature sat on.

What pooling does

Pooling (also called subsampling or downsampling) shrinks a feature map's height and width by summarising small regions. It has no learnable weights β€” it's a fixed rule. The two common kinds:

  • Max pooling β€” take the maximum value in each window. Keeps the strongest activation, i.e. "was the feature present anywhere in this region?" Most popular.
  • Average pooling β€” take the mean of the window. Smoother; common near the end of networks as global average pooling.
Worked example: 2Γ—2 max vs average pooling, stride 2
# INPUT feature map (4x4)
  1   3 | 2   4
  5   6 | 1   0
  -----+-----
  2   1 | 0   3
  7   8 | 4   2

# Split into four non-overlapping 2x2 windows (stride 2):
#   top-left {1,3,5,6}  top-right {2,4,1,0}
#   bot-left {2,1,7,8}  bot-right {0,3,4,2}

MAX POOL (2x2):          AVERAGE POOL (2x2):
   6    4                  3.75   1.75
   8    4                  4.50   2.25
# max of {1,3,5,6}=6     # mean of {1,3,5,6}=3.75

The 4Γ—4 map became 2Γ—2 β€” a 4Γ— reduction in values β€” while max pooling kept the strongest signal in each region.

Why pool at all?

Three reasons: (1) it reduces computation for later layers; (2) it gives a little extra translation tolerance β€” if the feature shifts by one pixel, the max of the window often doesn't change; (3) it gradually grows the receptive field so deeper layers see more of the image. Note: many modern networks instead use strided convolutions to downsample, but pooling remains a clear, classic tool you must know.

A typical conv block: Conv β†’ ReLU β†’ Pool

Convolutions are linear (just multiplies and adds). To learn non-linear patterns we add an activation function after each conv β€” almost always ReLU (from Session 3): ReLU(x) = max(0, x), which simply zeros out negatives. The classic building block of a CNN is therefore:

πŸ–ΌοΈ
Input
image / feature maps
β†’
πŸ”
Conv
filters detect patterns
β†’
⚑
ReLU
add non-linearity
β†’
πŸ”½
Pool
downsample
β†’
πŸ”
Repeat
stack more blocks

Stack several of these blocks and the spatial size keeps shrinking while the number of channels grows β€” the network trades "where" (resolution) for "what" (rich features). At the very end, the small feature stack is flattened and fed to a couple of fully-connected layers (the MLP returns!) to produce the final class scores. That whole pipeline is a Convolutional Neural Network (CNN).

Worked example: a conv block in PyTorch

Here's a real, runnable conv block using nn.Conv2d, with the shapes printed so you can trace the math from Topic 3:

import torch
import torch.nn as nn

# A fake batch: 1 image, 3 channels (RGB), 32x32 pixels
x = torch.randn(1, 3, 32, 32)   # shape (N, C, H, W)

# One conv block: Conv -> ReLU -> MaxPool
conv = nn.Conv2d(
    in_channels=3,      # RGB input
    out_channels=32,    # 32 filters -> 32 feature maps
    kernel_size=3,
    stride=1,
    padding=1,         # "same" padding for a 3x3 kernel -> H,W preserved
)
relu = nn.ReLU()
pool = nn.MaxPool2d(kernel_size=2, stride=2)  # halves H and W

out = pool(relu(conv(x)))

print("after conv:", conv(x).shape)   # [1, 32, 32, 32]  (padding kept 32x32)
print("after pool:", out.shape)        # [1, 32, 16, 16]  (pool halved to 16x16)

# Learnable parameters in this conv layer:
# 32 filters * (3 channels * 3 * 3) + 32 biases = 864 + 32 = 896
print("params:", sum(p.numel() for p in conv.parameters()))  # 896

Note PyTorch's tensor order is (N, C, H, W) β€” batch, channels, height, width. The output went from 3Γ—32Γ—32 to 32Γ—16Γ—16: more channels, smaller spatial size β€” exactly the "trade where for what" pattern.

Recap Pooling (max or average) downsamples feature maps with no learnable weights, cutting computation and adding shift tolerance. The classic CNN unit is Conv β†’ ReLU β†’ Pool, stacked repeatedly so spatial size shrinks while channel count grows, then capped with fully-connected layers for the final decision. In PyTorch, nn.Conv2d + nn.ReLU + nn.MaxPool2d builds exactly this block.

β˜… Putting it all together


You just built the entire foundation of computer vision with neural networks. Here's the one-paragraph story that connects all five topics:

A plain MLP drowns on images β€” it has a parameter explosion, it loses spatial structure when you flatten the grid, and it's not translation-invariant. The cure is the convolution: a small learned filter/kernel that slides over the image taking a dot product at each stop, producing a feature map of where its pattern appears. We tune it with kernel size, stride, padding (valid vs same) and depth, predicting the output with floor((Wβˆ’K+2P)/S)+1, while stacking layers grows the receptive field. Because of local connectivity and weight sharing, a conv layer has a tiny, image-size-independent parameter count yet finds features anywhere. We add ReLU for non-linearity and pooling to downsample, giving the classic Conv β†’ ReLU β†’ Pool block that β€” stacked and finished with a small MLP head β€” forms a CNN. Next session we'll assemble these blocks into the famous architectures.

Quick self-check

Name the three reasons a plain MLP struggles with images.

(1) Parameter explosion β€” flattening a big image gives millions of weights in layer one; (2) it loses spatial structure β€” flattening hides which pixels are neighbours; (3) it's not translation-invariant β€” it must relearn a pattern for each position.

What exactly is computed at each position when a filter slides over an image?

A dot product: multiply each kernel weight by the pixel beneath it and sum all the products into a single number. Collecting these numbers over all positions gives a feature map.

An input is 28Γ—28, kernel 3Γ—3, padding 1, stride 1. What is the output size?

floor((28 βˆ’ 3 + 2Γ—1)/1) + 1 = floor(27) + 1 = 28. So 28Γ—28 β€” "same" padding preserved the size (P = (kβˆ’1)/2 = 1 for a 3Γ—3 kernel).

Why does a conv layer have far fewer parameters than a fully-connected layer?

Weight sharing: the same small filter is reused at every position instead of a unique weight per connection. A conv layer's parameter count depends only on kernel size and channel counts, not on the image's height and width.

What does max pooling do, and what are its benefits?

It takes the maximum value in each small window, downsampling the feature map (e.g. 2Γ—2 stride 2 quarters the values). Benefits: less computation later, mild translation tolerance, and a growing receptive field. It has no learnable weights.

What are the layers in a classic CNN building block, and why is each there?

Conv (detect patterns with learned filters) β†’ ReLU (add non-linearity so the network can model complex shapes) β†’ Pool (downsample, cut cost, add shift tolerance). Stacked repeatedly, then a fully-connected head makes the final decision.

πŸ“š References & Further Reading


Class material

  • SST Deep Learning handout (Session 9) β€” your course handout for this session (covers convolutions, conv hyperparameters, weight sharing, and pooling).

Papers, docs & deep dives