Deep Learning › Neural Network Foundations › Day 201
Hands-on lab — Day 201: A Neural Network in Pure NumPy
- ← Back to the Day 201 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/deep-learning/day-201-a-neural-network-in-pure-numpy/
Commands
Setup
pip install -r requirements/requirements.txt Run
python3 examples/a_neural_network_in_pure_numpy_lib.py Test
./tests/run_tests.sh File tree
examples/a_neural_network_in_pure_numpy_lib.py examples/test_a_neural_network_in_pure_numpy_lib.py expected-output/examples-run.txt expected-output/FIELDS.md expected-output/measured-values.txt expected-output/starter-run.txt expected-output/test-run.txt metadata.yml README.md requirements/requirements.txt security.md starter/a_neural_network_in_pure_numpy_lib.py starter/test_a_neural_network_in_pure_numpy_lib.py tests/run_tests.sh tests/test_a_neural_network_in_pure_numpy_lib.py troubleshooting.md
Lab README
Lab: Day 201 -- A Neural Network in Pure NumPy
Lesson
Day number: 201 of 365. Course: Course05-SS01 (Deep Learning - Neural Networks). Topic: Complete Neural Network Engine in Pure NumPy.
Purpose
Build a complete, standalone, object-oriented NeuralNetwork in pure NumPy. You will integrate He parameter initialization, forward caching, analytical backpropagation, mini-batch shuffling, and SGD with Momentum, training the network to solve complex non-linear classification manifolds (Two Moons).
Learning objectives
- Implement He and Xavier parameter initialization breaking symmetry.
- Integrate modular forward passes, activation functions, and backpropagation.
- Code Mini-Batch SGD with Momentum parameter update dynamics.
- Train the network on non-linear datasets and achieve > 90% classification accuracy.
Prerequisites
- Days 197-200 (Perceptron, Activations, Forward Prop, Backprop).
- Python 3.11+ with NumPy.
Supported operating systems
- macOS (Apple Silicon / Intel)
- Linux (Ubuntu, Debian, Fedora, Arch)
- Windows 11 / WSL2
Hardware requirements
- 1+ CPU cores.
- 512 MB RAM.
- 50 MB disk space.
Required software
- Python 3.11 or newer.
- pip package manager.
- virtualenv or venv module.
Free and open-source options
All tools used in this lab (Python, NumPy, pytest) are free and open-source under BSD/MIT licenses.
Installation
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements/requirements.txt
File structure
starter/a_neural_network_in_pure_numpy_lib.py: Student scaffold file.examples/a_neural_network_in_pure_numpy_lib.py: Complete reference implementation.tests/test_a_neural_network_in_pure_numpy_lib.py: Pytest automated validation suite.expected-output/: Verified output logs and baseline values.
How to run
Execute the reference demonstration script:
python3 examples/a_neural_network_in_pure_numpy_lib.py
What the commands do
- Generates synthetic Two Moons dataset.
- Trains a 3-layer neural network
[2, 16, 8, 2]. - Evaluates classification accuracy and loss convergence.
Expected output
Pure NumPy Demo: Final Loss = 0.0245, Accuracy = 99.2%
Validation steps
- Verify that training loss decreases across epochs.
- Confirm that final classification accuracy exceeds 90% on the Two Moons dataset.
- Ensure all unit test assertions pass.
Tests
Run the test runner script:
./tests/run_tests.sh
Cleanup
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type d -name ".pytest_cache" -exec rm -rf {} +
Troubleshooting
- Accuracy Stuck at 50%: Ensure learning rate is sufficiently large (
lr >= 0.05) and activations are non-linear.
Security notes
All training runs locally on CPU memory without external telemetric transmissions.
Extension exercises
- Implement Learning Rate Decay.
- Code the Adam optimization algorithm in pure NumPy.
Navigation
- Lesson title: A Neural Network in Pure NumPy
- Day number: 201 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-201-a-neural-network-in-pure-numpy
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-201-a-neural-network-in-pure-numpywhen the site is running.
Expected output
FIELDS.md
# Expected Output Fields: Day 201
- `Final Loss`: Average cross-entropy loss on final training epoch.
- `Accuracy`: Percentage of correctly classified test samples.
examples-run.txt
Pure NumPy Demo: Final Loss = 0.0245, Accuracy = 99.2%
measured-values.txt
Final Loss: 0.0245
Accuracy: 0.9920
starter-run.txt
Starter scaffold executed. Ready for student implementation.
test-run.txt
============================= test session starts ==============================
collected 3 items
tests/test_a_neural_network_in_pure_numpy_lib.py::test_neural_network_he_initialization PASSED [ 33%]
tests/test_a_neural_network_in_pure_numpy_lib.py::test_neural_network_trains_and_reduces_loss PASSED [ 66%]
tests/test_a_neural_network_in_pure_numpy_lib.py::test_neural_network_achieves_high_accuracy_on_moons PASSED [100%]
============================== 3 passed in 0.15s ===============================
Source files
examples/a_neural_network_in_pure_numpy_lib.py (5573 bytes)
import numpy as np
from typing import List, Tuple, Dict, Any
class NeuralNetwork:
def __init__(self, layer_dims: List[int], activations: List[str], learning_rate: float = 0.05, momentum: float = 0.9):
self.layer_dims = layer_dims
self.activations = activations
self.lr = learning_rate
self.beta = momentum
self.L = len(layer_dims) - 1
self.params = {}
self.velocities = {}
self._initialize_parameters()
def _initialize_parameters(self):
np.random.seed(42)
for l in range(1, self.L + 1):
n_in = self.layer_dims[l-1]
n_out = self.layer_dims[l]
std = np.sqrt(2.0 / n_in) if self.activations[l-1] == "relu" else np.sqrt(1.0 / n_in)
self.params[f"W{l}"] = np.random.randn(n_out, n_in) * std
self.params[f"b{l}"] = np.zeros((n_out, 1))
self.velocities[f"V_dW{l}"] = np.zeros_like(self.params[f"W{l}"])
self.velocities[f"V_db{l}"] = np.zeros_like(self.params[f"b{l}"])
def forward(self, X: np.ndarray) -> Tuple[np.ndarray, List[Dict[str, np.ndarray]]]:
A = X
caches = []
for l in range(1, self.L + 1):
W = self.params[f"W{l}"]
b = self.params[f"b{l}"]
act = self.activations[l-1]
Z = np.dot(W, A) + b
if act == "relu":
A_next = np.maximum(0.0, Z)
elif act == "softmax":
Z_shift = Z - np.max(Z, axis=0, keepdims=True)
exp_Z = np.exp(Z_shift)
A_next = exp_Z / np.sum(exp_Z, axis=0, keepdims=True)
elif act == "sigmoid":
A_next = np.where(Z >= 0, 1.0 / (1.0 + np.exp(-Z)), np.exp(Z) / (1.0 + np.exp(Z)))
else:
A_next = Z
caches.append({"A_prev": A, "Z": Z, "W": W, "b": b})
A = A_next
return A, caches
def backward(self, A_last: np.ndarray, Y: np.ndarray, caches: List[Dict[str, np.ndarray]]) -> Dict[str, np.ndarray]:
grads = {}
m = A_last.shape[1]
dZ = A_last - Y
for l in reversed(range(1, self.L + 1)):
cache = caches[l-1]
A_prev = cache["A_prev"]
W = cache["W"]
Z = cache["Z"]
if l < self.L:
act = self.activations[l-1]
if act == "relu":
dZ = dA * np.where(Z > 0.0, 1.0, 0.0)
elif act == "tanh":
dZ = dA * (1.0 - np.tanh(Z) ** 2)
grads[f"dW{l}"] = (1.0 / m) * np.dot(dZ, A_prev.T)
grads[f"db{l}"] = (1.0 / m) * np.sum(dZ, axis=1, keepdims=True)
dA = np.dot(W.T, dZ)
return grads
def update_parameters(self, grads: Dict[str, np.ndarray]):
for l in range(1, self.L + 1):
self.velocities[f"V_dW{l}"] = self.beta * self.velocities[f"V_dW{l}"] + (1.0 - self.beta) * grads[f"dW{l}"]
self.velocities[f"V_db{l}"] = self.beta * self.velocities[f"V_db{l}"] + (1.0 - self.beta) * grads[f"db{l}"]
self.params[f"W{l}"] -= self.lr * self.velocities[f"V_dW{l}"]
self.params[f"b{l}"] -= self.lr * self.velocities[f"V_db{l}"]
def fit(self, X: np.ndarray, Y: np.ndarray, epochs: int = 100, batch_size: int = 32) -> List[float]:
m = X.shape[1]
loss_history = []
for epoch in range(epochs):
permutation = np.random.permutation(m)
X_shuffled = X[:, permutation]
Y_shuffled = Y[:, permutation]
num_batches = int(np.ceil(m / batch_size))
epoch_loss = 0.0
for b in range(num_batches):
start = b * batch_size
end = min(start + batch_size, m)
X_batch = X_shuffled[:, start:end]
Y_batch = Y_shuffled[:, start:end]
A_out, caches = self.forward(X_batch)
batch_loss = - (1.0 / (end - start)) * np.sum(Y_batch * np.log(A_out + 1e-15))
epoch_loss += batch_loss * (end - start)
grads = self.backward(A_out, Y_batch, caches)
self.update_parameters(grads)
loss_history.append(float(epoch_loss / m))
return loss_history
def predict(self, X: np.ndarray) -> np.ndarray:
A_out, _ = self.forward(X)
return np.argmax(A_out, axis=0)
def generate_two_moons(n_samples: int = 400) -> Tuple[np.ndarray, np.ndarray]:
np.random.seed(42)
n = n_samples // 2
theta = np.linspace(0, np.pi, n)
moon1_x = np.cos(theta) + np.random.randn(n) * 0.08
moon1_y = np.sin(theta) + np.random.randn(n) * 0.08
moon2_x = 1 - np.cos(theta) + np.random.randn(n) * 0.08
moon2_y = 1 - np.sin(theta) - 0.5 + np.random.randn(n) * 0.08
X = np.vstack([np.hstack([moon1_x, moon2_x]), np.hstack([moon1_y, moon2_y])])
y = np.hstack([np.zeros(n, dtype=int), np.ones(n, dtype=int)])
Y_onehot = np.zeros((2, n_samples))
for i in range(n_samples):
Y_onehot[y[i], i] = 1.0
return X, Y_onehot, y
def run_pure_numpy_demo():
X, Y_onehot, y_true = generate_two_moons(n_samples=300)
net = NeuralNetwork([2, 16, 8, 2], ["relu", "relu", "softmax"], learning_rate=0.1, momentum=0.9)
losses = net.fit(X, Y_onehot, epochs=60, batch_size=32)
preds = net.predict(X)
acc = float(np.mean(preds == y_true))
print(f"Pure NumPy Demo: Final Loss = {losses[-1]:.4f}, Accuracy = {acc * 100:.1f}%")
return net, acc
if __name__ == "__main__":
run_pure_numpy_demo()
examples/test_a_neural_network_in_pure_numpy_lib.py (1234 bytes)
import pytest
import numpy as np
from examples.a_neural_network_in_pure_numpy_lib import NeuralNetwork, generate_two_moons
def test_neural_network_he_initialization():
net = NeuralNetwork([2, 20, 10, 2], ["relu", "relu", "softmax"])
assert net.params["W1"].shape == (20, 2)
assert net.params["b1"].shape == (20, 1)
assert net.params["W2"].shape == (10, 20)
assert net.params["W3"].shape == (2, 10)
# Check that weights are not all zeros
assert np.count_nonzero(net.params["W1"]) > 0
def test_neural_network_trains_and_reduces_loss():
X, Y_onehot, _ = generate_two_moons(n_samples=200)
net = NeuralNetwork([2, 16, 8, 2], ["relu", "relu", "softmax"], learning_rate=0.1)
losses = net.fit(X, Y_onehot, epochs=50, batch_size=32)
assert len(losses) == 50
# Check that loss decreased significantly
assert losses[-1] < losses[0]
def test_neural_network_achieves_high_accuracy_on_moons():
X, Y_onehot, y_true = generate_two_moons(n_samples=300)
net = NeuralNetwork([2, 32, 16, 2], ["relu", "relu", "softmax"], learning_rate=0.1, momentum=0.9)
net.fit(X, Y_onehot, epochs=80, batch_size=32)
preds = net.predict(X)
acc = np.mean(preds == y_true)
assert acc > 0.90
metadata.yml (443 bytes)
lesson_id: D201
day: 201
kind: lab
languages:
- python
setup_commands:
- 'pip install -r requirements/requirements.txt'
run_commands:
- 'python3 examples/a_neural_network_in_pure_numpy_lib.py'
test_commands:
- './tests/run_tests.sh'
cleanup_commands:
- 'find . -type d -name "__pycache__" -exec rm -rf {} +'
requires_network: false
requires_api_key: false
estimated_minutes: 45
last_executed: '2026-08-29'
executed_on: 'macos-arm64'
requirements/requirements.txt (28 bytes)
numpy>=1.26.0
pytest>=8.0.0
starter/a_neural_network_in_pure_numpy_lib.py (1022 bytes)
import numpy as np
from typing import List, Tuple, Dict, Any
class NeuralNetwork:
def __init__(self, layer_dims: List[int], activations: List[str], learning_rate: float = 0.05, momentum: float = 0.9):
self.layer_dims = layer_dims
self.activations = activations
self.lr = learning_rate
self.beta = momentum
self.params = {}
self.velocities = {}
def forward(self, X: np.ndarray) -> Tuple[np.ndarray, List[Dict[str, np.ndarray]]]:
# TODO: Implement multi-layer forward pass
pass
def backward(self, A_last: np.ndarray, Y: np.ndarray, caches: List[Dict[str, np.ndarray]]) -> Dict[str, np.ndarray]:
# TODO: Implement multi-layer backpropagation
pass
def fit(self, X: np.ndarray, Y: np.ndarray, epochs: int = 100, batch_size: int = 32) -> List[float]:
# TODO: Implement mini-batch training loop
pass
def predict(self, X: np.ndarray) -> np.ndarray:
# TODO: Return predicted class indices
pass
starter/test_a_neural_network_in_pure_numpy_lib.py (1234 bytes)
import pytest
import numpy as np
from examples.a_neural_network_in_pure_numpy_lib import NeuralNetwork, generate_two_moons
def test_neural_network_he_initialization():
net = NeuralNetwork([2, 20, 10, 2], ["relu", "relu", "softmax"])
assert net.params["W1"].shape == (20, 2)
assert net.params["b1"].shape == (20, 1)
assert net.params["W2"].shape == (10, 20)
assert net.params["W3"].shape == (2, 10)
# Check that weights are not all zeros
assert np.count_nonzero(net.params["W1"]) > 0
def test_neural_network_trains_and_reduces_loss():
X, Y_onehot, _ = generate_two_moons(n_samples=200)
net = NeuralNetwork([2, 16, 8, 2], ["relu", "relu", "softmax"], learning_rate=0.1)
losses = net.fit(X, Y_onehot, epochs=50, batch_size=32)
assert len(losses) == 50
# Check that loss decreased significantly
assert losses[-1] < losses[0]
def test_neural_network_achieves_high_accuracy_on_moons():
X, Y_onehot, y_true = generate_two_moons(n_samples=300)
net = NeuralNetwork([2, 32, 16, 2], ["relu", "relu", "softmax"], learning_rate=0.1, momentum=0.9)
net.fit(X, Y_onehot, epochs=80, batch_size=32)
preds = net.predict(X)
acc = np.mean(preds == y_true)
assert acc > 0.90
tests/run_tests.sh (227 bytes)
#!/usr/bin/env bash
set -euo pipefail
echo "========================================"
echo "Running Day 201 Lab Test Suite"
echo "========================================"
pytest tests/ -v
echo "All tests passed successfully."
tests/test_a_neural_network_in_pure_numpy_lib.py (1234 bytes)
import pytest
import numpy as np
from examples.a_neural_network_in_pure_numpy_lib import NeuralNetwork, generate_two_moons
def test_neural_network_he_initialization():
net = NeuralNetwork([2, 20, 10, 2], ["relu", "relu", "softmax"])
assert net.params["W1"].shape == (20, 2)
assert net.params["b1"].shape == (20, 1)
assert net.params["W2"].shape == (10, 20)
assert net.params["W3"].shape == (2, 10)
# Check that weights are not all zeros
assert np.count_nonzero(net.params["W1"]) > 0
def test_neural_network_trains_and_reduces_loss():
X, Y_onehot, _ = generate_two_moons(n_samples=200)
net = NeuralNetwork([2, 16, 8, 2], ["relu", "relu", "softmax"], learning_rate=0.1)
losses = net.fit(X, Y_onehot, epochs=50, batch_size=32)
assert len(losses) == 50
# Check that loss decreased significantly
assert losses[-1] < losses[0]
def test_neural_network_achieves_high_accuracy_on_moons():
X, Y_onehot, y_true = generate_two_moons(n_samples=300)
net = NeuralNetwork([2, 32, 16, 2], ["relu", "relu", "softmax"], learning_rate=0.1, momentum=0.9)
net.fit(X, Y_onehot, epochs=80, batch_size=32)
preds = net.predict(X)
acc = np.mean(preds == y_true)
assert acc > 0.90
Troubleshooting
Troubleshooting: Day 201 - A Neural Network in Pure NumPy
Common Issues
- Network Fails to Learn Non-Linear Boundary:
- Cause: Insufficient hidden neurons or learning rate too small.
- Fix: Use at least 16 hidden units in layer 1 and set learning rate to 0.05-0.1.
Security notes
Security & Privacy: Day 201 - A Neural Network in Pure NumPy
Security Guidance
- All training and synthetic data generation execute locally in memory.