All projects
P13 / Course project · Deep learning · Group project2025

Neural networks in NumPy & JAX.

A fully connected neural network written from scratch in NumPy, ported to JAX for up to 4.2× faster training, and tuned through hyperparameter sweeps to 86.86% test accuracy on the 47-class EMNIST Balanced dataset.

DTU · COURSE 02456Deep Learning
PythonNumPyJAXWeights & BiasesGoogle ColabMNIST / EMNIST
First page of the project report, "Implementing a neural network from scratch with NumPy and JAX", DTU course 02456, autumn 2025.
First page of the project report (DTU 02456 Deep Learning, autumn 2025).

Final test accuracy

86.86%

EMNIST Balanced, 47 classes of handwritten digits and letters, with a fully connected network.

NumPy → JAX

Up to 4.2× faster

Per sweep run, from 19–23 minutes with NumPy on CPU to about 5.5 minutes with JAX on a GPU.

The goal

The aim of this DTU Deep Learning group project (course 02456, autumn 2025) was to develop a fully connected neural network without frameworks such as PyTorch or TensorFlow, with the forward pass, gradients and optimisers implemented by hand, and to push its performance as far as possible on handwritten digits (MNIST) and the harder 47-class EMNIST Balanced dataset of digits and letters.

My involvement

Contributed to the development of the original NumPy network, carried out the port to JAX, built the Weights & Biases sweep setup that drove the hyperparameter search, and carried out the search and optimisation of the final network.

From NumPy to JAX

The NumPy network supports configurable depth and width, three activation functions, three weight initialisations, SGD and Adam, weight decay and dropout. It was too slow for systematic sweeps, so it was rewritten in JAX, which keeps a NumPy-like interface but compiles the computation just-in-time and runs it on a GPU. The same sweep, run with both versions:

ActivationInitialisationNumPy · runtimeNumPy · val. acc.JAX · runtimeJAX · val. acc.
ReLUNormal19 m 6 s97.40%5 m 39 s93.82%
ReLUXavier18 m 53 s97.62%5 m 32 s95.75%
ReLUHe18 m 48 s97.55%5 m 29 s95.90%
SigmoidNormal21 m 2 s94.10%5 m 40 s87.08%
SigmoidXavier21 m 7 s94.80%5 m 39 s90.98%
SigmoidHe20 m 57 s94.83%5 m 49 s90.63%
TanhNormal23 m 22 s94.75%5 m 33 s94.63%
TanhXavier23 m 6 s95.20%5 m 30 s94.40%
TanhHe23 m 5 s95.33%5 m 51 s94.38%

MNIST, 2 × 128 hidden units, 100 epochs. NumPy on Google Colab Intel Xeon CPUs (batch 32); JAX on an NVIDIA T4 GPU (batch 512).

Each run became 3.4–4.2 times faster. The slightly lower JAX accuracies, most visible for sigmoid, come mainly from the larger GPU batches, which give 16 times fewer parameter updates per epoch.

ProcessorArchitectureCompute (FP32)MemoryUsed for
Intel Xeon (CPU)Skylake / Cascade Lake60–120 GFLOPS12–13 GB RAMNumPy sweeps
NVIDIA T4 (GPU)Turing (TU104)8.1 TFLOPS16 GB GDDR6JAX grid and first Bayesian sweeps
NVIDIA A100 (GPU)Ampere (GA100)19.5 TFLOPS40 GB HBM2eFinal Bayesian sweep

Google Colab hardware used for training. Specifications from NVIDIA and Google Cloud documentation.

Finding the best network

Every design choice was tested in sweeps rather than assumed:

  • Regularisation: Adam beat plain SGD but overfitted; 30% dropout removed the overfitting.
  • Activation and initialisation: ReLU clearly outperformed tanh and sigmoid on EMNIST (up to 77% against 70% and 62% for a small network), whose gradients saturate. He and Xavier initialisation performed almost identically.
  • Architecture: a Bayesian sweep of 36 runs on an NVIDIA A100 found the best results with about 1000–1500 hidden units in total and 30–40% dropout.

Result

The final network has three hidden layers of 512 ReLU units with 36% dropout, trained with Adam. It reached 86.86% test accuracy on EMNIST Balanced (87.16% validation) in a training time of 10.5 minutes.

Most remaining errors are characters that are genuinely ambiguous as isolated 28 × 28 images: F is read as f in 44% of test cases, L as 1 in 39% and q as 9 in 33%. A convolutional network, which keeps the image’s spatial structure, would be expected to do better; for a fully connected network, the result is close to the best achievable configuration.

Technical documentation

Source code on GitHub