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:
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:
| Layer | Output shape | Parameters |
|---|---|---|
| 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
- Compute the output shape of a 3×3 convolution with stride 2 and
padding="same"on a32×32×3input. - Why does a 5×5 convolution followed by 2×2 max pooling reach further than two 3×3 convolutions with pooling between them?
- 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.