Simple CNN — MNIST
A six-block, ~60K-parameter convolutional network reaches 99.2% test accuracy on handwritten digits — the cleanest place in this gallery to read a real architecture top to bottom, layer by layer, with every shape and parameter count fully accounted for.
- A 28×28 grayscale '7' enters on the left.
conv_block1andconv_block2each shrink the spatial size by 2 (no padding) while channels grow — the volume gets thinner and deeper. transition1halves the resolution again with a 2×2 max-pool, then a 1×1 convolution squeezes channels — a cheap reset before the next pair of 3×3 blocks.conv_block3andconv_block4repeat the pattern, ending at 8×8×32 — the last feature-map volume, and the tensor Grad-CAM reads from.- Global average pooling collapses 8×8×32 to a single 32-number summary, the 1×1 classifier turns that into 10 scores, and log-softmax turns the scores into the class the network is confident is a '7'.
Every architecture diagram in this gallery is really just a sequence of convolutions with a shape attached to each step. This model is small enough that you can hold the entire sequence in your head at once — six blocks, every one of them a 3×3 or 1×1 convolution, each shrinking or reshaping the volume by a fixed, computable amount. That makes it the right place to build the habit this page is for: at every step, ask what is the shape now, and how many numbers does this step cost.
The input is a single 28×28 grayscale image — one channel, so the input volume is . Trace it block by block, and every number below comes straight from the trained checkpoint's layer definitions:
| Block | Operation | Output shape | Params |
|---|---|---|---|
| Input | — | — | |
conv_block1 | Conv 3×3, , no padding + BN + ReLU | 144 | |
conv_block2 | Conv 3×3, , no padding + BN + ReLU + Dropout | 4,608 | |
transition1 | MaxPool 2×2, stride 2, then Conv 1×1, + BN + ReLU | 512 | |
conv_block3 | Conv 3×3, , no padding + BN + ReLU + Dropout | 4,608 | |
conv_block4 | Conv 3×3, , no padding + BN + ReLU + Dropout | 9,216 | |
| GAP + classifier | Global avg pool to , then Conv 1×1, | (log-probs) | 320 |
Two shape rules explain every entry in that output shape column, and both are the convolution arithmetic you've seen before, applied with . A plain 3×3 convolution with no padding and stride 1 takes to — each 3×3 block in this network costs exactly 2 pixels off each spatial dimension. A 2×2 max-pool with stride 2 halves the spatial size outright. Run those two rules down the table: (two plain convs), (the pool in transition1, the 1×1 conv that follows it doesn't change spatial size), (two more plain convs). Every number in the output shape column is one of those two operations, nothing else.
What a feature map is, here. After conv_block1, the volume is — sixteen separate grids, one per output channel, each the record of where one learned 3×3 pattern fired across the digit. A pixel at position in channel 7 is the dot product of that channel's kernel with the patch of the input centred near — exactly the arithmetic from the convolution page, just with so the kernel volume is , the simplest possible case. By conv_block4, each of the 32 channels in the volume is the result of four convolutions stacked — so a single cell there is a function of a much larger patch of the original 28×28 image than the 3×3 a first-layer cell sees. That growth is the receptive field at work, and it is exactly why the last block is where Grad-CAM looks (see the Interpretability tab).
Now size one layer exactly, the way you would read any PyTorch Conv2d definition. conv_block4 maps input channels to output channels with a kernel and bias=False. The parameter-count formula for a convolutional layer is when there's a bias term per output channel — drop the here because this network's convolutions carry no bias (BatchNorm supplies the shift instead):
9,216 weights, and — exactly as the convolution page's formula promises — not one of them depends on whether the feature map is 8×8 or 800×800. The same 32 kernels would cost 9,216 weights on any input size.
That single block is the single most expensive layer in the network: 9,216 of the model's roughly 60,000 total parameters, just under one-sixth, in one 3×3-by-32-by-32 layer. Compare it to transition1's 1×1 convolution — — which does real work (it halves the channel count from 32 to 16) for an eighteenth of the cost. That gap is the entire reason 1×1 convolutions exist as a design tool: spatial kernels () are where the parameter budget goes, and convolutions are how architects buy channel-mixing almost for free.
The head: from a volume to ten numbers. After conv_block4, the network holds an volume — 2,048 numbers, far too many to feed straight into a 10-way decision without either an enormous dense layer or throwing away spatial information on purpose. This network does the second: global average pooling (GAP) replaces each of the 32 channels' grid with a single number, its mean. The intuition is direct — if a channel is a detector for some learned pattern, its average response across the whole image is a reasonable summary of "how much of this pattern is present, anywhere." GAP turns , with zero learned parameters — it's an architectural choice, not a layer you train.
The final step is a convolution mapping channels — parameters — which, on a spatial input, is just a linear layer wearing a convolution's clothes: each of the 10 outputs is a weighted sum of the 32 pooled channel values. Those 10 raw scores (logits) pass through log-softmax, which turns them into log-probabilities — ten numbers that sum, after exponentiating, to 1. The predicted digit is simply the index of the largest one. Log-softmax instead of plain softmax is a numerical-stability choice that also pairs cleanly with the negative-log-likelihood loss used during training; it changes nothing about which class wins, only how the winning margin is represented.
- check_circle~60K parameters total — small enough that the whole table above accounts for essentially every weight in the model.
- check_circleNo padding anywhere — every 3×3 convolution shrinks its input by 2 pixels per side, which is why without a single explicit crop.
- check_circleOne pooling layer — a single 2×2 max-pool in
transition1does all the spatial downsampling; everything else is convolution. - check_circleGAP, not flatten — the head never flattens the 8×8×32 volume into a long vector, which is exactly what keeps the parameter count near 60K instead of the millions a flatten + dense head would cost.
A 99.2%-accurate model is a black box unless you can ask it why. Grad-CAM (Gradient-weighted Class Activation Mapping) is the standard answer for convolutional networks: it turns the model's internal feature maps into a heatmap over the input image, showing which pixels the model's decision actually depended on. This model ships Grad-CAM as its one interpretability capability — the workspace runs it live on any digit you draw or upload.
Grad-CAM needs two things: a feature-map volume to explain, and a gradient telling it which channels in that volume mattered for the predicted class. For this model both come from conv_block4's output — the volume from the Architecture tab, the last spatial feature map before global average pooling discards position entirely. Picking the last convolutional block is deliberate: it's the deepest representation that still has spatial structure, so its activations are the most semantically rich map that can still be drawn as a picture.
Here is the computation, in the same notation as the rest of this page. Let be the -th channel of conv_block4's output (so , each an grid), and let be the log-softmax score for the predicted class (one of the 10 digits). Grad-CAM first asks: if channel 's activations all increased slightly, how much would the score for class change? That's a gradient, averaged over the grid into one importance weight per channel:
One number per channel — how much channel , on average, pushes the score toward class . This is exactly the backward-pass machinery from the vanishing-gradients page, run for one specific purpose: not to update weights, but to read out an importance score.
Then the heatmap is a weighted sum of the 32 channel maps, passed through a ReLU to keep only the positive evidence — pixels that pushed toward the predicted class, not away from it:
An heatmap, one value per spatial cell of conv_block4's output. The workspace upsamples this grid back to and overlays it on the input digit.
Why the heat lands on ink strokes. Each cell of that grid corresponds to a patch of the original 28×28 image — by conv_block4, after four 3×3 convolutions and one 2×2 pool, each cell's receptive field covers a sizeable neighbourhood, large enough to contain a stroke segment or a junction between strokes. The channels with the largest are the ones whose detectors — built from the edge- and stroke-shaped kernels learned in the early blocks (the same vocabulary described in the convolution feature-hierarchy) — fired most strongly and most usefully for distinguishing this digit from the other nine. For a clean, centred digit, that combination reliably picks out the strokes themselves: a '7' lights up along its horizontal bar and diagonal, a '0' lights up as a ring along its loop. Background pixels — uniform, featureless — produce near-zero activation in every channel, so is small there regardless of the weights, and the ReLU clips any residual negative response to exactly zero.
This is also a sanity check, not just a pretty picture. If Grad-CAM on a correctly-classified '7' highlighted the corners of the image instead of the stroke, that would be evidence the model is exploiting some dataset artefact (a border effect, a watermark) rather than the digit's shape — the kind of failure that test-set accuracy alone can hide. Draw a digit with an unusual stroke style in the workspace and watch whether the heat still tracks your ink: that's the model's 99.2% made legible, one prediction at a time.