Kiru Lab  /  Deep Learning  /  Architectures as Priors

Convolution: Locality Made Structural

A convolutional layer is a fully connected layer with most weights forced to zero and the rest tied together.

Concept  ·  about 30 minutes

A convolution slides a small filter across an input and computes a dot product at each position. Two structural constraints define it: locality, because each output sees only a small neighborhood; and weight sharing, because the same filter is applied everywhere.

Both constraints are claims about the world. Locality claims that nearby inputs are more related than distant ones. Weight sharing claims that a pattern worth detecting in one location is worth detecting in all of them — translation invariance. For natural images both hold. For tabular data neither does, which is why convolution is the wrong tool there.

Fully connected on 224x224x3: ~150,000 weights per output unit 3x3 convolution, 64 filters: 1,728 weights, reused at every position

The learned hierarchy

Stack convolutions and the receptive field grows with depth. Early layers reliably learn edge and color-gradient detectors; middle layers learn textures and simple shapes; late layers learn object parts. Nobody specified this. It emerges from the structure plus the data, and it is one of the strongest empirical results in the field.

Structure dictates function

The mammalian visual system shows the same progression: oriented edge detectors in V1, texture and contour selectivity in V2 and V4, object-level selectivity in inferotemporal cortex. Two systems built by completely different processes converged on the same layered decomposition, which suggests the hierarchy is a property of the problem rather than of either implementation.


Hold on to

  • Convolution encodes locality and translation invariance structurally
  • Weight sharing is a prior about the world, not a memory optimization
  • Edge-to-texture-to-object hierarchies emerge in both CNNs and visual cortex

Work through

Try each one before opening the solution. Getting it wrong first is most of where the learning happens.

  1. Implement 2D convolution with explicit loops, then verify against a library version.
    Hint

    Slide the kernel over every valid position and take an elementwise product-sum.

    Solution

    The explicit version makes the two structural constraints visible: the inner loop only ever touches a small neighborhood (locality), and the same `kernel` array is used at every position (weight sharing). Note the output is smaller than the input unless you pad — that shrinkage is why deep convolutional stacks need padding to preserve spatial dimensions.

    import numpy as np
    from scipy.signal import convolve2d
    
    def conv2d(image, kernel):
        kh, kw = kernel.shape
        h, w = image.shape[0] - kh + 1, image.shape[1] - kw + 1
        out = np.zeros((h, w))
        for i in range(h):
            for j in range(w):
                out[i, j] = np.sum(image[i:i + kh, j:j + kw] * kernel)
        return out
    
    rng = np.random.default_rng(0)
    image, kernel = rng.normal(size=(20, 20)), rng.normal(size=(3, 3))
    reference = convolve2d(image, kernel[::-1, ::-1], mode="valid")  # note the flip
    assert np.allclose(conv2d(image, kernel), reference)
    Check your work

    Paste this after your own code. If it runs without raising, you have it.

    import numpy as np
    image = np.arange(16, dtype=float).reshape(4, 4)
    kernel = np.ones((2, 2))
    out = conv2d(image, kernel)
    assert out.shape == (3, 3)
    assert out[0, 0] == 0 + 1 + 4 + 5
    print("ok")
  2. Visualize the first-layer filters of a pretrained CNN. Count how many are recognizable edge detectors.
    Hint

    Reshape the first conv layer weights to (out_channels, 3, kh, kw) and show them as RGB images.

    Solution

    In a trained network typically half to two-thirds of first-layer filters are recognizable oriented edge detectors — bright on one side, dark on the other, at various angles — and most of the rest are color-opponent blobs. This closely matches what Hubel and Wiesel measured in cat V1 in 1959, from a system that learned it from ImageNet rather than from evolution. Two very different processes, the same solution.

    import matplotlib.pyplot as plt
    import torchvision
    
    model = torchvision.models.resnet18(weights="DEFAULT")
    filters = model.conv1.weight.detach().numpy()      # (64, 3, 7, 7)
    
    fig, axes = plt.subplots(8, 8, figsize=(8, 8))
    for f, ax in zip(filters, axes.ravel()):
        f = (f - f.min()) / (f.max() - f.min())
        ax.imshow(f.transpose(1, 2, 0)); ax.axis("off")
    plt.tight_layout(); plt.show()

In the Bio Mirror

The Visual Hierarchy

Sign in to track your progress through the lab.