Code

Train the CNN and see what it really bought

All 13 tests pass — so the CNN exists. Time to train it.

What changes in train.py

Almost nothing. fit, evaluate and the loop have no idea about pictures; they only know "model → logits". All that is needed is for build_model to be able to create a CNN:

from model import CNN, MLP, LinearClassifier, count_parameters
...
    if name == "cnn":
        return CNN(dropout=dropout)
...
    parser.add_argument("--model", choices=["linear", "mlp", "cnn"], default="mlp")

In the right panel you see three files in our versions: model.py with the CNN filled in, conv_by_hand.py with the holes filled and pooling written, and train.py with this change. If yours passes the tests, keep yours — these are only for comparison.

Run it

python train.py --model cnn --epochs 15 --dropout 0.3

With our random seed (torch.manual_seed(42)) and 10,000 training images we got:

cnn: 20,490 parameters
epoch  1  loss 0.6056  train acc 76.5%  |  val loss 0.6426  val acc 74.5%
epoch  2  loss 0.4953  train acc 82.4%  |  val loss 0.5401  val acc 80.8%
epoch  3  loss 0.4497  train acc 83.6%  |  val loss 0.5035  val acc 81.7%
   ...
epoch 13  loss 0.2729  train acc 90.8%  |  val loss 0.3562  val acc 87.5%
epoch 14  loss 0.2728  train acc 90.4%  |  val loss 0.3651  val acc 87.4%
epoch 15  loss 0.2769  train acc 89.9%  |  val loss 0.3647  val acc 87.4%
final: train 89.9%  val 87.4%  gap +2.5%

The duration depends on your CPU: our measurements ranged from ~20 seconds (a quiet CPU) to 66 seconds (with three other runs sharing it).

Your numbers will differ by roughly 1–2 points. The same machine with the same seed reproduces numbers exactly, but another machine or another PyTorch version does not quite. What matters is the order, not the third digit.

What it means

model parameters train val gap
MLP (256, 256), 30 ep. (lesson 7) 269,322 97.1 % 86.7 % +10.4
MLP (256, 256) + dropout + weight decay + early stopping 269,322 89.4 % 86.0 % +3.3
CNN (16, 32), dropout 0.3, 15 ep. 20,490 89.9 % 87.4 % +2.5

Read it carefully, because it is easy to overstate:

  • The CNN beat the MLP by 0.7 points in one seed. Over three seeds (0, 1, 2), on average: MLP 86.2 %, MLP with regularisation 86.5 %, CNN 87.5 %; the seed-to-seed spread is ~0.6 points. That is roughly +1 to +1.5 points — real, but not a jump.
  • And it did it with 13 times fewer parameters (20,490 vs 269,322).
  • The train-to-validation gap is 2.5 points instead of 10.4: a smaller network has less to memorise.

Equal budget — this is where the difference is

The fair comparison is the same parameter count. An MLP with a single hidden layer of 25 neurons has 19,885 parameters, nearly the same as the CNN:

python train.py --model mlp --hidden 25 --epochs 15
python train.py --model cnn --epochs 15
mlp: 19,885 parameters
   ...
final: train 87.2%  val 84.1%  gap +3.1%

cnn: 20,490 parameters
   ...
final: train 91.0%  val 87.2%  gap +3.8%

(Both without dropout, 15 epochs, seed 42.) That is +3.1 points on validation for the same number of parameters. This is where structure pays off.

What to take away

On this task the CNN is not magic: 28 × 28 grayscale clothes are not where it shines. But it wins when the budget is limited, and it is practically unavoidable when pictures are large (224 × 224 × 3 = 150,528 inputs — a dense layer with 100 neurons would have over 15 million weights). Those are exactly the pictures we move to in lessons 9 and 10.