TAI BUI
Back to curriculum
BuildPython

Convolutions from Scratch

A convolution is a tiny dense layer, slid across an image, reusing the same weights at every position it lands on.

TIME: ~75 minutesPREREQUISITES: Phase 3 (Deep Learning Core), Phase 4 Lesson 01 (Image Fundamentals)

Learning Objectives

  • Implement 2D convolution from scratch using only NumPy, including the nested-loop version and a vectorised `im2col` version
  • Compute output spatial size for any combination of input size, kernel size, padding, and stride, and justify the `(H - K + 2P) / S + 1` formula
  • Hand-design kernels (edge, blur, sharpen, Sobel) and explain why each one produces the pattern of activations it does
  • Stack convolutions into a feature extractor and connect the depth-of-the-stack to the size of the receptive field

The Problem

A fully connected layer on a 224x224 RGB image needs 224 * 224 * 3 = 150,528 input weights per neuron. A single hidden layer with 1,000 units is already 150 million parameters, and that's before the network has learned anything useful. Worse, that layer has no notion that a dog in the top-left and a dog in the bottom-right are the same pattern. It treats every pixel position as independent, which is exactly wrong for images: translating a cat by three pixels shouldn't force the network to relearn the concept.

The two properties an image model needs are translation equivariance (the output shifts when the input shifts) and parameter sharing (the same feature detector runs everywhere). Dense layers give you neither. Convolution gives you both for free.

Convolution wasn't invented for deep learning. It's the same operation behind JPEG compression, Gaussian blur in Photoshop, edge detection in industrial vision, and every audio filter ever shipped. CNNs dominated ImageNet from 2012 to 2020 because convolution is the correct prior for data where nearby values are related and the same pattern can show up anywhere.

Warm-up0/3
1. You compute the sum of the element-wise products of [1, 2, 3] and [0, 1, 0]. What is the result?Warm-up
Two lists of three numbers, A = 1, 2, 3 and B = 0, 1, 0, stacked so each position lines up, with the sum of their paired products left blank.
2. A dense layer takes a flattened input of 100 values and has 10 neurons, each with one weight per input. Ignoring biases, how many weights does it hold?Warm-up
A dense layer spec listing 100 flattened inputs and 10 neurons, each neuron holding one weight per input, with the total weight count left blank.
3. You multiply a matrix of shape (8, 27) by a matrix of shape (27, 256). What is the shape of the result?Warm-up
Two matrices labelled only by shape, one 8 by 27 and one 27 by 256, lined up for a matrix multiply with the result shape left blank.
Check: The Problem0/3
1. What does translation equivariance mean for a conv layer?Define
Two runs of the same conv layer drawn as input and output rows of cells. In the first run the one bright input cell is at column 2, in the second at column 5, and each run's output row is shown beneath its input.
2. An image model needs two properties that dense layers lack and convolution provides for free. Which two?Explain
The same small orange pattern drawn in the top-left corner of one image frame and in the bottom-right corner of another, with a note that a dense layer stores a separate weight for every pixel position.
3. A fully connected layer on a 224x224 RGB image needs 150,528 input weights per neuron. What does a single hidden layer of 1,000 such units cost?Apply
A dense layer spec listing 150,528 weights per neuron and 1,000 neurons, with the total parameter count left blank.

The Concept

One kernel, sliding

A 2D convolution takes a small weight matrix called the kernel, or filter, slides it across the input, and at each position sums the element-wise products between the kernel and whatever window it's sitting over. That sum becomes one output pixel.

Mermaid diagram

A concrete 3x3 example on a 5x5 input (no padding, stride 1):

Input X (5 x 5):                Kernel W (3 x 3):

  1  2  0  1  2                   1  0 -1
  0  1  3  1  0                   2  0 -2
  2  1  0  2  1                   1  0 -1
  1  0  2  1  3
  2  1  1  0  1

The kernel slides across every valid 3 x 3 window. Output Y is 3 x 3:

 Y[0,0] = sum( W * X[0:3, 0:3] )
 Y[0,1] = sum( W * X[0:3, 1:4] )
 Y[0,2] = sum( W * X[0:3, 2:5] )
 Y[1,0] = sum( W * X[1:4, 0:3] )
 ... and so on

That's the whole idea in one line: shared weights, locality, a sliding window. Everything else is bookkeeping.

Output size formula

Given input spatial size H, kernel size K, padding P, stride S:

H_out = floor( (H - K + 2P) / S ) + 1

Memorise this. You will compute it dozens of times per architecture.

ScenarioHKPSH_out
Valid conv, no padding3230130
Same conv (preserves size)3231132
Downsample by 23231216
Pool 2x23220216
Large receptive field3273216

"Same padding" means picking P so that H_out equals H when S == 1. For odd K, that's P = (K - 1) / 2, which is why 3x3 kernels dominate: they're the smallest odd kernel that still has a centre.

Padding

Padding adds extra border values, usually zero, around the input before the kernel slides over it. Skip it and every convolution shrinks the feature map: stack 20 of them and your 224x224 image becomes 184x184, which wastes compute on the border and complicates residual connections that need matching shapes.

Zero padding (P = 1) on a 5 x 5 input:

  0  0  0  0  0  0  0
  0  1  2  0  1  2  0
  0  0  1  3  1  0  0
  0  2  1  0  2  1  0       Now the kernel can centre on pixel
  0  1  0  2  1  3  0       (0, 0) and still have three rows and
  0  2  1  1  0  1  0       three columns of values to multiply.
  0  0  0  0  0  0  0

You'll meet four modes in practice: zero (most common), reflect (mirrors the edge, useful for avoiding hard borders in generative models), replicate (copies the edge), and circular (wraps around, used in toroidal problems).

Stride

Stride is the step size of the slide. stride=1 is the default. stride=2 halves the spatial dimensions, the classic way to downsample inside a CNN without a separate pooling layer: every modern architecture (ResNet, ConvNeXt, MobileNet) uses strided convs in place of max-pool somewhere.

Stride 1 on a 5 x 5 input, 3 x 3 kernel:

  starts: (0,0) (0,1) (0,2)        -> output row 0
          (1,0) (1,1) (1,2)        -> output row 1
          (2,0) (2,1) (2,2)        -> output row 2

  Output: 3 x 3

Stride 2 on the same input:

  starts: (0,0) (0,2)              -> output row 0
          (2,0) (2,2)              -> output row 1

  Output: 2 x 2

Multiple input channels

Real images have three channels. A 3x3 convolution on an RGB input is actually a 3x3x3 volume: one 3x3 slice per input channel. At each spatial position, you multiply and sum across all three slices and add a bias.

Input:   (C_in,  H,  W)        3 x 5 x 5
Kernel:  (C_in,  K,  K)        3 x 3 x 3 (one kernel)
Output:  (1,     H', W')       2D map

For a layer that produces C_out output channels, you stack C_out kernels:

Weight:  (C_out, C_in, K, K)   e.g. 64 x 3 x 3 x 3
Output:  (C_out, H', W')       64 x 3 x 3

Parameter count: C_out * C_in * K * K + C_out   (the + C_out is biases)

That last line is the one you'll calculate when planning a model. A 64-channel 3x3 conv on a 3-channel input has 64 * 3 * 3 * 3 + 64 = 1,792 parameters. Cheap.

The im2col trick

im2col flattens every receptive-field window of the input into one column of a big matrix, flattens the kernel into a row, and turns the whole convolution into a single matmul. The motivation: nested loops are easy to read but slow, and GPUs want big matrix multiplies, not four levels of Python for-loops.

cols, h_out, w_out = im2col(x, kh, kw, stride, padding)   # (C_in*kh*kw, H_out*W_out)
out = w.reshape(c_out, -1) @ cols                          # one matmul, not 4 nested loops

Mermaid diagram

Analogy: picture photocopying every 3x3 patch of the image onto its own index card, then laying every card side by side as one column of a giant spreadsheet. One matrix multiply, run once over the whole spreadsheet, now answers what would otherwise take thousands of small dot products computed one patch at a time.

Every production conv implementation is some variant of this, plus cache-tiling tricks (direct conv, Winograd, FFT conv for large kernels). Understand im2col and you understand the core of how convolution actually runs on hardware.

Receptive field

The receptive field of a neuron is the patch of the original input that its activation actually depends on, and it grows with depth. A single 3x3 conv looks at 9 input pixels. Stack two 3x3 convs and a neuron in the second layer looks at 5x5 input pixels. Three 3x3 convs give 7x7. In general:

RF after L stacked K x K convs (stride 1) = 1 + L * (K - 1)

With strides:   RF grows multiplicatively with stride along each layer.

That's the entire reason "3x3 all the way down" works (VGG, ResNet, ConvNeXt): two 3x3 convs see the same input area as one 5x5 conv, but with fewer parameters and an extra non-linearity in between.

Check: The Concept0/24
1. What is a 2D convolution in a CNN?Define
A 5 by 5 input grid with its top-left 3 by 3 window outlined, next to a 3 by 3 kernel, with an arrow to one resulting output cell.
2. What is a kernel (also called a filter) in a conv layer?Define
The nine values of W, the 3 by 3 grid from the lesson's worked example, beside a short note of its size.
3. What does stride control in a convolution?Define
A row of cells with a 3-wide window outlined at its first placement and again at the placement right after it, drawn once for stride 1 and once for stride 2.
illustrative row, not a specific layer's size
4. What is padding in a conv layer?Define
The lesson's 5 by 5 input X drawn inside a dashed ring of extra cells that each hold a zero, the P equals 1 diagram from the Concept section.
5. What is the receptive field of a neuron in a conv network?Define
The lesson's 5 by 5 input X with one 3 by 3 window outlined and an arrow running from that window to a single output unit y.
6. What does the im2col transformation actually do?Define
A 4 by 4 grid with two overlapping 2 by 2 windows marked in different colours, and an arrow to the first two columns of the matrix the lesson's im2col returns, each holding one window's four values.
7. What is a depthwise convolution?Define
Three input channel boxes on the left, three output channel boxes on the right, and one straight arrow from each input channel to the output channel with the same index.
illustrative channel count
8. What does setting stride=2 do to a conv layer's output, and why do modern architectures use it?Explain
A feature map labelled N by N, an arrow marked stride 2 convolution, pointing to a second feature map whose size is left as a question mark.
9. Besides zero padding, which padding mode mirrors the edge values, which helps avoid hard borders in generative models?Explain
A 3 by 3 numeric input surrounded by a dashed ring of sixteen border cells, every one of them left as a question mark.
10. Why do modern CNNs prefer stacks of 3x3 convolutions over a single 5x5 or 7x7 conv?Explain
Two stacked 3 by 3 conv layers on the left and a single 5 by 5 conv on the right, with the choice between them left open.
11. Why are 3x3 kernels so common when a layer must keep the spatial size at stride 1?Explain
The size-preserving row of the lesson's output-size table, H 32, K 3, P 1, S 1 and H_out 32, with the reason 3 by 3 is picked so often left open.
12. Why would you replace the nested-loop convolution with the im2col version?Explain
Two boxes, one for the lesson's conv2d_naive with its four nested Python loops and one for its conv2d_im2col with a single matrix multiply.
13. Why do CNNs pad their inputs instead of letting every conv shrink the feature map?Explain
A chain of conv layers each labelled smaller than the last after the input, with no padding shown between them.
14. You feed a 224x224 RGB image to a conv with kernel_size=3, stride=1, padding=0. What is the output spatial size?Apply
A conv layer spec listing a 224 by 224 RGB input, kernel size 3, stride 1 and padding 0, with the output spatial size left blank.
15. A conv layer has in_channels=3, out_channels=64, kernel_size=3, with bias. How many learnable parameters?Apply
A conv layer spec with in_channels 3, out_channels 64, kernel size 3 and bias enabled, with the parameter count left blank.
16. You stack two 5x5 convolutions (both stride 1, no pooling). What is the receptive field of a neuron in the second layer?Apply
Two 5 by 5 conv layers in a row at stride 1, with the receptive field of a neuron in the second layer left blank.
17. You want 'same' padding (output size equal to input size) with stride 1 and a 5x5 kernel. What padding value should you use?Apply
A conv layer spec with a 5 by 5 kernel at stride 1 whose output must be the same size as its input, with the padding value left blank.
18. Using H_out = floor((H - K + 2P) / S) + 1, what is the output size for H = 32, K = 7, P = 3, S = 2?Apply
The output-size formula with H equal to 32, K equal to 7, P equal to 3 and S equal to 2 substituted in, and the result left blank.
19. A 3x3 kernel slides over a 5x5 input with no padding and stride 2. What is the output size?Apply
A 5 by 5 grid of empty cells with one 3 by 3 window outlined, labelled stride 2 and no padding, with the output size left blank.
20. A network stacks 20 unpadded 3x3 convs on a 224x224 input, and a residual add fails with a shape mismatch. What is the most likely cause?Analyze
A 224 by 224 input feeding 20 stacked 3 by 3 convolutions with padding 0, a skip branch running around them, and the residual add at the end marked as the line that fails.
21. A teammate says four stacked 3x3 stride-1 convs have a receptive field of 12x12 because 4 * 3 = 12. What is the flaw?Analyze
A boxed claim reading RF equals 4 times 3 equals 12, above a chain of four stride-1 3 by 3 conv layers.
22. You are limited to 3x3 convs with stride 1 and need a final neuron that sees at least a 7x7 patch of the input. What is the smallest stack that works?Design
A requirement box reading receptive field at least 7 by 7, kernel 3, stride 1 throughout, with the number of layers needed left blank.
23. You must halve a 32x32 feature map inside a CNN without adding a separate pooling layer. Which conv setting does that?Design
A 32 by 32 feature map and a 16 by 16 feature map with one conv layer between them, the conv's kernel size, padding and stride left blank.
24. A 64-to-64 channel 3x3 conv uses too many parameters for a mobile model. Which change cuts the count most while keeping the sliding-window structure?Design
A 64 to 64 channel 3 by 3 conv labelled with the regular weight formula C_out times C_in times K times K, with the cheaper replacement left blank.

Build It

Step 1: Pad an array

Start with the smallest primitive: a function that pads with zeros around an H x W array.

python
import numpy as np
 
def pad2d(x, p):
    if p == 0:
        return x
    h, w = x.shape[-2:]
    out = np.zeros(x.shape[:-2] + (h + 2 * p, w + 2 * p), dtype=x.dtype)
    out[..., p:p + h, p:p + w] = x
    return out
 
x = np.arange(9).reshape(3, 3)
print(x)
print()
print(pad2d(x, 1))

The trailing-axes trick x.shape[:-2] means the same function works on (H, W), (C, H, W), or (N, C, H, W) without modification.

Step 2: 2D convolution with nested loops

The reference implementation: slow, but unambiguous. This is what torch.nn.functional.conv2d does in principle.

python
def conv2d_naive(x, w, b=None, stride=1, padding=0):
    c_in, h, w_in = x.shape
    c_out, c_in_w, kh, kw = w.shape
    assert c_in == c_in_w
 
    x_pad = pad2d(x, padding)
    h_out = (h + 2 * padding - kh) // stride + 1
    w_out = (w_in + 2 * padding - kw) // stride + 1
 
    out = np.zeros((c_out, h_out, w_out), dtype=np.float32)
    for oc in range(c_out):
        for i in range(h_out):
            for j in range(w_out):
                hs = i * stride
                ws = j * stride
                patch = x_pad[:, hs:hs + kh, ws:ws + kw]
                out[oc, i, j] = np.sum(patch * w[oc])
        if b is not None:
            out[oc] += b[oc]
    return out

Four nested loops (output channel, row, column, plus the implicit sum over C_in, kh, kw). This is the ground truth you'll check every faster implementation against.

Step 3: Verify with a hand-designed kernel

Build a vertical Sobel kernel, apply it to a synthetic step image, and watch the vertical edge light up.

python
def synthetic_step_image():
    img = np.zeros((1, 16, 16), dtype=np.float32)
    img[:, :, 8:] = 1.0
    return img
 
sobel_x = np.array([
    [[-1, 0, 1],
     [-2, 0, 2],
     [-1, 0, 1]]
], dtype=np.float32)[None]
 
x = synthetic_step_image()
y = conv2d_naive(x, sobel_x, padding=1)
print(y[0].round(1))

Expect large positive values on column 7 (left-to-right brightness increase) and zeros everywhere else. That single print is your sanity check that the math is right.

Step 4: im2col

Convert every kernel-sized window in the input into a column of a matrix. For C_in=3, K=3, each column is 27 numbers.

python
def im2col(x, kh, kw, stride=1, padding=0):
    c_in, h, w = x.shape
    x_pad = pad2d(x, padding)
    h_out = (h + 2 * padding - kh) // stride + 1
    w_out = (w + 2 * padding - kw) // stride + 1
 
    cols = np.zeros((c_in * kh * kw, h_out * w_out), dtype=x.dtype)
    col = 0
    for i in range(h_out):
        for j in range(w_out):
            hs = i * stride
            ws = j * stride
            patch = x_pad[:, hs:hs + kh, ws:ws + kw]
            cols[:, col] = patch.reshape(-1)
            col += 1
    return cols, h_out, w_out

It's still a Python loop, but the heavy lifting is now a single vectorised matmul.

Step 5: Fast conv via im2col + matmul

Replace the quadruple loop with one matrix multiplication.

python
def conv2d_im2col(x, w, b=None, stride=1, padding=0):
    c_out, c_in, kh, kw = w.shape
    cols, h_out, w_out = im2col(x, kh, kw, stride, padding)
    w_flat = w.reshape(c_out, -1)
    out = w_flat @ cols
    if b is not None:
        out += b[:, None]
    return out.reshape(c_out, h_out, w_out)

Correctness check: run both implementations and compare.

python
rng = np.random.default_rng(0)
x = rng.normal(0, 1, (3, 16, 16)).astype(np.float32)
w = rng.normal(0, 1, (8, 3, 3, 3)).astype(np.float32)
b = rng.normal(0, 1, (8,)).astype(np.float32)
 
y_naive = conv2d_naive(x, w, b, padding=1)
y_im2col = conv2d_im2col(x, w, b, padding=1)
 
print(f"max abs diff: {np.max(np.abs(y_naive - y_im2col)):.2e}")

max abs diff should land around 1e-5. That gap comes from floating-point accumulation order, not a bug.

Step 6: A bank of hand-designed kernels

Five filters that show what a single conv layer can express before any training.

python
KERNELS = {
    "identity": np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]], dtype=np.float32),
    "blur_3x3": np.ones((3, 3), dtype=np.float32) / 9.0,
    "sharpen": np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], dtype=np.float32),
    "sobel_x": np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32),
    "sobel_y": np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32),
}
 
def apply_kernel(img2d, kernel):
    x = img2d[None].astype(np.float32)
    w = kernel[None, None]
    return conv2d_im2col(x, w, padding=1)[0]

Applied to any grayscale image, blur softens, sharpen crisps up edges, Sobel-x lights up vertical edges, Sobel-y lights up horizontal edges. These are exactly the patterns the first trained conv layer in AlexNet and VGG ended up learning, because any good image model needs edge and blob detectors no matter what task comes later.

Check: Build It0/7
1. Why does the sobel_x kernel [[-1,0,1],[-2,0,2],[-1,0,1]] light up on a vertical edge but give zero on a flat patch?Explain
The nine values of the lesson's sobel_x kernel in a 3 by 3 grid, matching the kernel printed in the question, beside a note of where the lesson uses it.
2. In the Build It step 3 test, a synthetic image is 0 on the left half and 1 on the right half, and you convolve it with sobel_x. What should the output look like?Apply
A 16 by 16 image split into a dark left half and a light right half, next to the sobel_x kernel, with an arrow to an output left blank.
3. With the lesson's pad2d, x = np.arange(9).reshape(3, 3), and p = 1, what is the shape of pad2d(x, 1)?Apply
A 3 by 3 grid holding 0 through 8, labelled as the input to pad2d with p equal to 1, with the resulting shape left blank.
4. The lesson's im2col is called on an input of shape (3, 16, 16) with kh = kw = 3, stride 1 and padding 1. What shape does cols have?Apply
A call to the lesson's im2col with a 3 by 16 by 16 input, kernel 3 by 3, stride 1 and padding 1, with the resulting cols shape left blank.
5. You compare conv2d_naive and conv2d_im2col on the same random input and the max abs difference is about 1e-5. What does that tell you?Analyze
A terminal showing the real printed output of the lesson's correctness check, a matching output shape and a tiny maximum absolute difference.
real run of the lesson's main.py, the rest of its output cut
6. conv2d_naive raises an AssertionError on an RGB input of shape (3, 16, 16) with weights of shape (8, 1, 3, 3). What is wrong?Analyze
A terminal showing conv2d_naive called with an input of shape 3 by 16 by 16 and weights of shape 8 by 1 by 3 by 3, raising an AssertionError, with no further detail shown.
7. You apply sobel_x to an image of horizontal stripes (brightness changes only from top to bottom) and get near zero everywhere. What does this indicate?Analyze
A 16 by 16 image drawn as four alternating horizontal bands, next to the sobel_x kernel, with an arrow to an output left blank.

Use It

PyTorch's nn.Conv2d wraps the same operation with autograd, CUDA kernels, and cuDNN optimisation. Shape semantics are identical.

python
import torch
import torch.nn as nn
 
conv = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, stride=1, padding=1)
print(conv)
print(f"weight shape: {tuple(conv.weight.shape)}   # (C_out, C_in, K, K)")
print(f"bias shape:   {tuple(conv.bias.shape)}")
print(f"param count:  {sum(p.numel() for p in conv.parameters())}")
 
x = torch.randn(8, 3, 224, 224)
y = conv(x)
print(f"\ninput  shape: {tuple(x.shape)}")
print(f"output shape: {tuple(y.shape)}")

Swap padding=1 for padding=0 and the output drops to 222x222. Swap stride=1 for stride=2 and it drops to 112x112. Same formula you memorised above.

Check: Use It0/1
1. The Use It example runs nn.Conv2d(3, 64, kernel_size=3, padding=1) on a 224x224 input. If you change stride=1 to stride=2, what is the output spatial size?Apply
The Use It example's conv layer with its stride changed from 1 to 2 on a 224 by 224 input, with the new output size left blank.

Ship It

This lesson produces:

  • outputs/prompt-cnn-architect.md: a prompt that, given input size, parameter budget, and target receptive field, designs a stack of Conv2d layers with the right K/S/P at every step.
  • outputs/skill-conv-shape-calculator.md: a skill that walks a network spec layer by layer and returns the output shape, receptive field, and parameter count for every block.
Author's Note

The first time I implemented convolution by hand, the nested-loop version felt like the whole story. I assumed the fast version PyTorch ships was a fundamentally different algorithm underneath. It isn't. Every production conv kernel, cuDNN, MKL-DNN, whatever your compiler lowers to, is some variant of im2col plus a tuned matmul, or a Winograd trick that's really im2col's cleverer cousin. Once you've built im2col yourself, nn.Conv2d stops being a black box and becomes "the version of my code that someone spent a decade optimising." That distinction matters the day a custom conv layer runs ten times slower than it should: you already know where to look, because you've written both the slow version and the fast one, and you watched the second one win.

Exercises

  1. (Easy) Given a 128x128 grayscale input and a stack of [Conv3x3(s=1,p=1), Conv3x3(s=2,p=1), Conv3x3(s=1,p=1), Conv3x3(s=2,p=1)], compute the output spatial size and the receptive field at each layer by hand. Verify with a PyTorch nn.Sequential of dummy convs.
  2. (Medium) Extend conv2d_naive and conv2d_im2col to accept a groups argument. Show that groups=C_in=C_out reproduces a depthwise convolution and that its parameter count is C * K * K instead of C * C * K * K.
  3. (Hard) Implement the backward pass of conv2d_im2col by hand: given the gradient of the output, compute the gradient of x and w. Verify against torch.autograd.grad on the same inputs and weights. The trick: the gradient of im2col is col2im, and it has to accumulate overlapping windows.

Key Terms

TermWhat people sayWhat it actually means
Convolution"Sliding a filter"A learnable dot product applied at every spatial location with shared weights; mathematically a cross-correlation, but everyone calls it convolution
Kernel / filter"The feature detector"A small weight tensor of shape (C_in, K, K) whose dot product with a window of input produces one output pixel
Stride"How far you jump"The step size between consecutive kernel placements; stride 2 halves each spatial dimension
Padding"Zeros on the edges"Extra values added around the input so the kernel can centre on border pixels; same padding keeps output size equal to input size
Receptive field"How much the neuron sees"The patch of original input that a given output activation depends on, growing with depth and stride
im2col"The GEMM trick"Rearranging every receptive window into columns so convolution becomes one big matrix multiply: the core of every fast conv kernel
Depthwise conv"One kernel per channel"A conv with groups == C_in, computing each output channel from only its matching input channel; the backbone of MobileNet and ConvNeXt
Translation equivariance"Shift in, shift out"Property that shifting the input by k pixels shifts the output by k pixels; comes for free with shared weights

Further Reading