Status: Verified (re-run 2026-10-14, seeds 0, 1, 2, validation split = last 10,000 of the 60,000 training images).
The original notebook, CNN_Identify_the_Digits_(MNIST).ipynb, passes the 10,000 test images to model.fit as validation_data. This run trains the same network with a validation set taken from the training data instead, evaluates the test set once at the end, and compares the result with the original 0.9905 (log: 2026-10-14-cnn-mnist.md). The original notebook and its stored outputs were not changed.
| Item | Original notebook | This run |
|---|---|---|
| Architecture | Conv2D 32 (3x3) - Conv2D 64 (3x3) - MaxPool 2x2 - Dropout 0.25 - Flatten - Dense 256 - Dropout 0.5 - Dense 10 softmax, 2,380,938 parameters | identical |
| Optimiser, loss | Adam (defaults), categorical cross-entropy | identical |
| Batch size, epochs | 10, 5 | identical |
| Training images | 60,000 | 50,000 (the first 50,000 of the training set; 5,000 steps per epoch, 25,000 in total) |
validation_data |
the 10,000 test images | the last 10,000 training images (a fixed block, not shuffled; label counts 991, 1064, 990, 1030, 983, 915, 967, 1090, 1009, 961 for digits 0 to 9) |
| Test set | used as validation data during training, then evaluated | not touched until after training, then one prediction pass gives the loss, accuracy and report |
| Seeds | none | random.seed, numpy.random.seed, tf.random.set_seed and keras.utils.set_random_seed all set to the seed; PYTHONHASHSEED=0. Seeds 0, 1 and 2 |
| Early stopping, checkpoints, model selection | none | none (the model evaluated is the epoch-5 model) |
Two files hold the code: scripts/mnist_validation_split.py (one seed per process, writes a JSON result; used for seeds 0, 1 and 2) and CNN_Identify_the_Digits_(MNIST)_validation_split.ipynb (single-seed demonstration, seed 0, outputs stored).
AMD Ryzen 7 5800X (8 cores, 16 threads), 32 GB RAM, Windows 11, no GPU used (TensorFlow reported no GPU devices). Python 3.11.9, TensorFlow 2.15.1, Keras 2.15.0, NumPy 1.26.4 (the earlier environment, versions as in requirements.txt).
KERAS_HOME=<local cache> PYTHONHASHSEED=0 timeout 1200 python scripts/mnist_validation_split.py --seed 0 --out seed0.json
KERAS_HOME=<local cache> PYTHONHASHSEED=0 timeout 1200 python scripts/mnist_validation_split.py --seed 1 --out seed1.json
KERAS_HOME=<local cache> PYTHONHASHSEED=0 timeout 1200 python scripts/mnist_validation_split.py --seed 2 --out seed2.json
KERAS_HOME=<local cache> PYTHONHASHSEED=0 timeout 1200 python -m jupyter nbconvert --to notebook --execute --inplace \
--ExecutePreprocessor.timeout=1200 --ExecutePreprocessor.kernel_name=python3 CNN_Identify_the_Digits_(MNIST)_validation_split.ipynb
The four runs were made one after another (never at the same time) on a copy in a scratch folder. All four exited with code 0, each well inside the 20-minute limit.
| Seed | Final train acc | Final validation acc | Test accuracy | Test loss | Test correct | Wall time | Fit time |
|---|---|---|---|---|---|---|---|
| 0 | 0.9889 | 0.9901 | 0.9907 | 0.03185 | 9,907 / 10,000 | 364 s | 360 s |
| 1 | 0.9889 | 0.9901 | 0.9890 | 0.03728 | 9,890 / 10,000 | 355 s | 352 s |
| 2 | 0.9893 | 0.9892 | 0.9887 | 0.03436 | 9,887 / 10,000 | 321 s | 318 s |
| Mean (sample SD) | 0.9890 (0.0002) | 0.9898 (0.0009) | 0.9895 (0.0011) | 0.03450 |
Wall time includes loading the data and the test pass. The notebook (seed 0) took 346 s from start to finish (fit 318 s) and printed test accuracy 0.9907, test loss 0.031851209700107574. These match the script’s seed-0 run in every printed per-epoch number and in the test loss to full precision, so a fixed seed reproduced the result on this machine and software.
| Epoch | Seed 0 train / val | Seed 1 train / val | Seed 2 train / val |
|---|---|---|---|
| 1 | 0.9510 / 0.9863 | 0.9515 / 0.9859 | 0.9510 / 0.9860 |
| 2 | 0.9777 / 0.9878 | 0.9793 / 0.9873 | 0.9789 / 0.9884 |
| 3 | 0.9833 / 0.9897 | 0.9839 / 0.9889 | 0.9850 / 0.9903 |
| 4 | 0.9860 / 0.9898 | 0.9882 / 0.9903 | 0.9879 / 0.9902 |
| 5 | 0.9889 / 0.9901 | 0.9889 / 0.9901 | 0.9893 / 0.9892 |
Per-epoch time was 62 to 75 s (13 to 15 ms per step).
| Test accuracy | |
|---|---|
| Original (test set as validation data, 60,000 training images, no seed, one run) | 0.9905 (9,905 / 10,000) |
| This run, mean of seeds 0, 1, 2 (validation split, 50,000 training images) | 0.9895 (range 0.9887 to 0.9907) |
| Difference, new mean minus original | -0.0010 (about 10 test images out of 10,000) |
What the numbers allow us to say:
Read cell by cell (cells 15 to 41 of the committed notebook):
model.fit(..., validation_data=(x_test, y_test)) only to print and plot val_loss and val_accuracy per epoch. Keras does not train on validation data, so the weights are not updated from it.EarlyStopping, ModelCheckpoint, ReduceLROnPlateau or other callback, no hyperparameter loop and no model comparison. model.save stores the epoch-5 model, and the final model.evaluate scores that same model. The last validation accuracy (0.9905) and the reported test accuracy (0.9905) are the same number because they are the same images and the same weights.batch_size = 128 and epochs = 10, which the training cell does not use (it passes epochs=5, batch_size=10), so an earlier configuration was changed at some point, and the reason is not recorded. The earlier stored output (test accuracy 0.8815) came from a different run. If those choices were made while watching the test curves, the test set would no longer be a perfectly independent measure; that cannot be checked from the repository.