THE MATHS / LEARNING NOTES
The mathematics of a tiny neural network
This lab learns a function from four examples. The purpose is to understand each calculation well enough to explain it, check it, and change it yourself. You need matrix multiplication, derivatives, and the chain rule. The Python syntax is explained alongside the mathematics.
Read this guide with core.py open. Work through
the exercises after the numerical example.
1. The problem: XOR
XOR means “exactly one input is 1.” Its complete Boolean truth table is:
| First input | Second input | Target output |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
We supply these four input rows and four targets. The network must learn parameters that give the right outputs. It is not given the symbolic XOR rule.
A single affine score cannot separate these labels with a straight line. To see
why, suppose a score is
Adding the middle two inequalities gives
2. What the words mean
- A weight multiplies an incoming value. Learning changes that multiplier.
- A bias adds an offset, so a neuron does not have to pass through the origin.
- A neuron computes a weighted sum plus a bias, then applies a function.
- A layer computes several neurons together using a matrix operation.
- An activation is that function. Here the hidden layer uses
tanh. - A parameter is a value we train: every weight and every bias.
- A hyperparameter is a choice we make before training, such as the hidden width or learning rate.
- A loss is one number measuring how badly the predictions match the targets.
- An epoch here is one update using all four examples. Other programs may process several smaller batches during an epoch.
Our architecture is 2 inputs → 4 hidden neurons → 1 output by default. You
can change the hidden width. The examples below call this width
3. Organize the numbers into arrays
Let
| Mathematical name | Python name | Shape | Meaning |
|---|---|---|---|
X |
Input rows | ||
y |
Target labels | ||
W1 |
Input-to-hidden weights | ||
b1 |
Hidden biases | ||
z1 |
Hidden values before activation | ||
h |
Hidden values after tanh |
||
W2 |
Hidden-to-output weights | ||
b2 |
Output bias | ||
z2 |
Output scores, also called logits | ||
p |
Predicted probabilities for label 1 |
For
In NumPy, @ means matrix multiplication; * means elementwise multiplication.
W.T transposes a matrix. Adding a
4. Forward propagation: calculate a prediction
The complete forward pass is:
where
These activation functions act on each element independently. tanh maps real
numbers into
The hidden neurons transform the original two coordinates into nonlinear
features. The output combines those features. Removing tanh would make the
two matrix multiplications collapse into one affine function, bringing back
the XOR limitation above.
The prediction rule is label 1 when
Network.forward(X) returns the intermediate arrays as well as p, allowing
you to inspect each stage instead of treating prediction as a black box.
5. Loss: score a prediction smoothly
For a target
If the target is 1, this is
The batch loss is the mean:
Accuracy only records whether a prediction crossed the threshold. Loss also records movement within either side of that threshold, giving training useful derivatives.
Why the code uses logits
Directly taking logs of rounded probabilities can produce log(0). Substituting
The implementation evaluates this as:
np.mean(np.logaddexp(0.0, z2) - y * z2)
np.logaddexp(a, b)
computes the log of the sum of the exponentials. Using it avoids explicitly
forming a potentially enormous exp(z2). Sigmoid is also evaluated with
separate positive/negative branches to avoid overflowing an exponential.
These choices preserve the mathematical model while improving numerical
behavior; they do not make arbitrarily large intermediate matrix products safe.
6. Backpropagation: calculate how each parameter affects the loss
Backpropagation is the chain rule applied in reverse order, reusing values from
the forward pass. A gradient has the same shape as its parameter. For example,
dW1[i, j] tells us how the loss changes to first order when W1[i, j] changes.
Begin with one example. Combining cross-entropy with sigmoid gives a useful simplification:
so
This derivation assumes interior probabilities. The implementation obtains the
same gradient directly as p - y, avoiding divisions by probabilities that may
round to zero.
For the mean loss on
Here
The corresponding NumPy operations are:
dz2 = (p - y) / len(X)
dW2 = h.T @ dz2
db2 = dz2.sum(axis=0, keepdims=True)
dz1 = (dz2 @ W2.T) * (1.0 - h * h)
dW1 = X.T @ dz1
db1 = dz1.sum(axis=0, keepdims=True)
axis=0 sums down the examples. keepdims=True retains the leading dimension
of size 1, preserving the bias shape. Divide by dz2; the later
sums already carry that factor. Dividing again would shrink the gradient
incorrectly.
All gradients refer to the same current parameter values. Calculate the entire
gradient before updating any parameter: changing W2 early would make the
hidden-layer calculation use the wrong weights.
7. Gradient descent: turn derivatives into learning
For every parameter array
where
For a sufficiently small step on a smooth loss,
The initialization seed selects reproducible starting weights. A seed is not a quality score. Biases can start at zero, but independently initialized weights let the hidden neurons learn different features.
The code uses Glorot/Xavier uniform initialization. For a layer with
The scale accounts for the number of incoming and outgoing connections, helping avoid starting with activations that are excessively large or small. It is an initialization heuristic, not a guarantee of convergence. The same seed and settings support repeatable comparisons in the same numerical environment; small floating point differences can occur across platforms or library versions.
8. A numerical example you can check by hand
Use two hidden neurons for this example, one input row
Forward propagation gives:
| Quantity | Value, rounded |
|---|---|
Since this batch has one row,
Both hidden derivatives have factor
The second row of dW1 is zero because the second input is zero in this example.
That does not prevent those weights from learning from other examples.
One simultaneous update with
Recomputing the forward pass gives
Reproduce the calculation using the library:
import numpy as np
from neural_math_lab.core import Network
net = Network(hidden=2, seed=7)
net.parameters["W1"][:] = [[0.5, -0.5], [1.0, 1.0]]
net.parameters["b1"][:] = [[0.0, 0.0]]
net.parameters["W2"][:] = [[1.0], [-1.0]]
net.parameters["b2"][:] = [[0.0]]
X = np.array([[1.0, 0.0]])
y = np.array([[1.0]])
print(net.forward(X))
print("Before:", net.loss(X, y))
grads = net.gradients(X, y)
print(grads)
net.step(grads, learning_rate=0.1)
print("After:", net.loss(X, y))
9. Check the derivative without trusting the derivation
For each individual parameter
Only that coordinate changes; all other parameters stay fixed. Compare this central difference with the analytical backpropagation gradient. A good agreement across every parameter is stronger evidence than merely seeing a decreasing training loss: a partly incorrect derivative might still reduce loss.
Run:
python -m neural_math_lab gradient-check
The perturbation should be small, but not arbitrarily tiny. Central differences
have approximation error, while subtracting nearly equal floating point losses
has roundoff error. The default
10. Train, inspect, and interpret honestly
python -m neural_math_lab train --epochs 4000 --learning-rate 0.5 --hidden 4 --seed 7 --output runs/xor
python -m neural_math_lab explain --model runs/xor/model.json --input 1 0
Read the training loss curve, then inspect the four truth-table predictions.
The explain command shows the saved model's actual intermediate numbers for
one input. Compare its values to the equations above; they differ from the hand
example because training learns different weights.
Every XOR row is used to train this first lab. Consequently:
- 100% XOR training accuracy means all four training rows were classified correctly. It is not held-out test accuracy.
- There is no unseen Boolean XOR input left after fitting all four rows.
- The colored prediction map between Boolean points shows the network's smooth extension of the function. Those locations have no supplied ground-truth labels in this experiment.
- A lower loss at the same training accuracy can mean predictions became more extreme; it is not independent evidence of better generalization.
A later experiment with many distinct samples should use training, validation, and untouched test data. For now, understanding the computation and verifying the gradients is the main result.
Further reading
- Stanford CS231n: backpropagation and the chain rule develops the computational-graph view used in this guide.
- NumPy
logaddexpreference documents the numerical operation used in our loss. - PyTorch automatic differentiation is a useful next step after calculating the derivatives yourself. This lab's gradients are written manually; it does not require PyTorch.