Skip to main content
Fanout
Convolutional Neural Network
Curriculum overview

TensorFlow Fundamentals · lesson 02/27

Convolutional Neural Network

A convolutional network is what happens when you stop treating an image as a flat vector of pixels and start treating it as a grid with local structure. Weight sharing across spatial positions gives you far fewer parameters and tolerance to small translations. A small CNN trains on MNIST in a couple of minutes on a laptop CPU.

The idea

A convolution slides a small kernel across the input and computes a dot product at each position. Kernel size, stride, and padding decide the output size:

out=WK+2PS+1\text{out} = \left\lfloor \frac{W - K + 2P}{S} \right\rfloor + 1

Each output channel is one learned filter, and the same filter is reused at every location. That is the whole point — a cat detector should not have to be relearned for the top-left and the bottom-right of an image. Stacking convolutions grows the receptive field: a unit in a deep layer sees a larger region of the input than a unit in the first layer. Pooling does no learning at all; it just enlarges the receptive field and shrinks the grid.

Worked example

Track shapes through a classic MNIST CNN with batch size 64:

LayerOutput shapeParameters
Input[64, 28, 28, 1]0
Conv 5×5, 16, valid[64, 24, 24, 16]416
MaxPool 2×2[64, 12, 12, 16]0
Conv 5×5, 32, valid[64, 8, 8, 32]12832
MaxPool 2×2[64, 4, 4, 32]0
Flatten[64, 512]0
Dense 128[64, 128]65664
Dense 10[64, 10]1290

The first convolution has 5 × 5 × 1 × 16 + 16 = 416 weights. For the receptive field: after conv1 it is 5, after the pool it is 6 with jump 2, and after conv2 it is 6 + (5 − 1) × 2 = 14, so a deep unit covers a 14×14 patch of the original image.

In code

import tensorflow as tf

model = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(28, 28, 1)),
    tf.keras.layers.Conv2D(16, 5, padding="valid", activation="relu"),
    tf.keras.layers.MaxPool2D(2),
    tf.keras.layers.Conv2D(32, 5, padding="valid", activation="relu"),
    tf.keras.layers.MaxPool2D(2),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(128, activation="relu"),
    tf.keras.layers.Dense(10),
])
model.compile(optimizer="adam",
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
              metrics=["accuracy"])

padding="valid" adds nothing, so the output shrinks at every convolution. padding="same" pads enough to keep the size at roughly W/S. The Flatten after convolution throws away spatial structure and is the single largest source of parameters here; modern architectures replace it with global average pooling.

Check yourself

  1. Compute the output shape of a 3×3 convolution with stride 2 and padding="same" on a 32×32×3 input.
  2. Why does a 5×5 convolution followed by 2×2 max pooling reach further than two 3×3 convolutions with pooling between them?
  3. What breaks if every convolution in a very deep network uses padding="valid"?

Key takeaways

  • Convolution shares weights across space; pooling shrinks the grid and enlarges the receptive field.
  • Shape arithmetic is (W − K + 2P)/S + 1; get it right before debugging anything else.
  • The dense head usually dominates the parameter count — global pooling is the usual fix.