Models / Simple CNN
code
grid_viewBaseline Classifier · 01

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.

analytics99.2% Acc60K paramsMNIST
Concepts in play
play_circleOne digit's shape, through all six blocks to a 10-way decision
  1. A 28×28 grayscale '7' enters on the left. conv_block1 and conv_block2 each shrink the spatial size by 2 (no padding) while channels grow 1→16→321 \to 16 \to 32 — the volume gets thinner and deeper.
  2. transition1 halves the resolution again with a 2×2 max-pool, then a 1×1 convolution squeezes 32→1632 \to 16 channels — a cheap reset before the next pair of 3×3 blocks.
  3. conv_block3 and conv_block4 repeat the pattern, ending at 8×8×32 — the last feature-map volume, and the tensor Grad-CAM reads from.
  4. 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'.
scienceOpen Workspace

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 28×28×128 \times 28 \times 1. Trace it block by block, and every number below comes straight from the trained checkpoint's layer definitions:

BlockOperationOutput shapeParams
Input—28×28×128 \times 28 \times 1—
conv_block1Conv 3×3, 1→161 \to 16, no padding + BN + ReLU26×26×1626 \times 26 \times 16144
conv_block2Conv 3×3, 16→3216 \to 32, no padding + BN + ReLU + Dropout24×24×3224 \times 24 \times 324,608
transition1MaxPool 2×2, stride 2, then Conv 1×1, 32→1632 \to 16 + BN + ReLU12×12×1612 \times 12 \times 16512
conv_block3Conv 3×3, 16→3216 \to 32, no padding + BN + ReLU + Dropout10×10×3210 \times 10 \times 324,608
conv_block4Conv 3×3, 32→3232 \to 32, no padding + BN + ReLU + Dropout8×8×328 \times 8 \times 329,216
GAP + classifierGlobal avg pool to 1×11{\times}1, then Conv 1×1, 32→1032 \to 101010 (log-probs)320
Six blocks, six shape changes. Notice the rhythm: a pair of plain 3×3 blocks (channels grow, spatial size shrinks by 2 each time because there is no padding), then a cheap 2×2 pool + 1×1 channel-mix to reset, repeated once more, then a global-pooling head.

Two shape rules explain every entry in that output shape column, and both are the convolution arithmetic you've seen before, applied with p=0p=0. A plain 3×3 convolution with no padding and stride 1 takes n×nn \times n to (n−2)×(n−2)(n-2) \times (n-2) — 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: 28→26→2428 \to 26 \to 24 (two plain convs), →12\to 12 (the pool in transition1, the 1×1 conv that follows it doesn't change spatial size), →10→8\to 10 \to 8 (two more plain convs). Every number in the output shape column is one of those two operations, nothing else.

filter_center_focusConcept refresherConvolutionOne small grid of weights, slid across every position, turns pixels into feature maps by dot product.Revisitarrow_forward

What a feature map is, here. After conv_block1, the volume is 26×26×1626 \times 26 \times 16 — sixteen separate 26×2626 \times 26 grids, one per output channel, each the record of where one learned 3×3 pattern fired across the digit. A pixel at position (i,j)(i, j) in channel 7 is the dot product of that channel's kernel with the 3×33 \times 3 patch of the input centred near (i,j)(i,j) — exactly the arithmetic from the convolution page, just with Cin=1C_{\text{in}}=1 so the kernel volume is 3×3×13 \times 3 \times 1, the simplest possible case. By conv_block4, each of the 32 channels in the 8×8×328 \times 8 \times 32 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 3232 input channels to 3232 output channels with a 3×33 \times 3 kernel and bias=False. The parameter-count formula for a convolutional layer is Cout(k⋅k⋅Cin+1)C_{\text{out}}(k \cdot k \cdot C_{\text{in}} + 1) when there's a bias term per output channel — drop the +1+1 here because this network's convolutions carry no bias (BatchNorm supplies the shift instead):

params(conv_block4)  =  Cout⋅(k⋅k⋅Cin)  =  32×(3×3×32)  =  32×288  =  9,216\text{params}(\text{conv\_block4}) \;=\; C_{\text{out}} \cdot (k \cdot k \cdot C_{\text{in}}) \;=\; 32 \times (3 \times 3 \times 32) \;=\; 32 \times 288 \;=\; 9{,}216

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 — 16×(1×1×32)=51216 \times (1 \times 1 \times 32) = 512 — 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 (k>1k>1) are where the parameter budget goes, and 1×11 \times 1 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 8×8×328 \times 8 \times 32 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' 8×88 \times 8 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 8×8×32→1×1×328 \times 8 \times 32 \to 1 \times 1 \times 32, with zero learned parameters — it's an architectural choice, not a layer you train.

The final step is a 1×11 \times 1 convolution mapping 32→1032 \to 10 channels — 10×(1×1×32)=32010 \times (1 \times 1 \times 32) = 320 parameters — which, on a 1×11 \times 1 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 28→26→24→⋯→828 \to 26 \to 24 \to \dots \to 8 without a single explicit crop.
  • check_circleOne pooling layer — a single 2×2 max-pool in transition1 does 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.

Sources & further reading