Multilayer Perceptrons and Backpropagation
Build a one-hidden-layer multilayer perceptron from scratch: derive forward and backward passes, check gradients, and train with mini-batches.
Machine Learning from ScratchAll chapters Multilayer Perceptrons and BackpropagationFull chapter · Sections in this chapter1 / 10 ↓
Reading tools Definitions, links & notes
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Multilayer Perceptrons and Backpropagation
A logistic regression model using the original input features draws a linear decision boundary. In the noisy two-moons example below, a straight line cuts across the crescent-shaped groups. A multilayer perceptron (MLP) can represent a curved boundary by stacking affine transformations with nonlinear activation functions. The first layer learns a new representation of the inputs, and the final layer makes a prediction from that representation.
The central difficulty is training. The hidden-layer network has more parameters than the linear baseline, and its loss is generally nonconvex in the combined layer parameters. We fit it with iterative gradient updates. Backpropagation computes the loss gradient with respect to every weight and bias by applying the chain rule systematically to the computation graph.
In this chapter we build a one-hidden-layer network from primitive NumPy operations. We derive the forward pass, the stable binary cross-entropy loss, and every gradient used in training. We check the gradients with finite differences, train the network with mini-batches, and compare it with logistic regression on a noisy nonlinear classification task. The chapter assumes familiarity with gradient descent and numerical optimization and with train/test discipline from validation strategies and data leakage.
Advantages
The main advantage of an MLP is representational flexibility. A hidden layer applies several learned affine projections followed by a nonlinearity, so the network can construct features that are not present in the raw input coordinates. In a two-dimensional classification problem, this lets the decision boundary bend. In higher-dimensional tabular problems, it lets the model represent interactions such as "high income matters only when the account is new." Logistic regression on the original features has additive log-odds; to represent an income-by-account-age interaction in the log-odds, it needs additional interaction or nonlinear features.
The same hidden-layer machinery extends to several prediction tasks. A sigmoid output with binary cross-entropy gives binary classification; a softmax output with cross-entropy gives multiclass classification; a linear output with squared error gives regression. The output dimension is chosen for the target representation: one logit for binary classification, one logit per class for a softmax output, and one value per regression target. The output activation and loss must match that task. The hidden-layer chain-rule calculation follows the same principle.
The forward and backward passes can be expressed with matrix multiplications and elementwise operations, with examples arranged in batches. Mini-batch training uses fewer examples for each update than full-batch gradient descent and averages more per-example gradients than one-example stochastic gradient descent. Regularization can be added directly to the loss, and validation data can monitor the resulting training loop.
Disadvantages
MLPs are nonconvex. The loss surface can contain many local minima, flat regions, saddle points, and steep cliffs. Gradient descent does not come with a global convergence guarantee. In practice, careful initialization, input scaling, sensible learning rates, and mini-batch noise make training work well on many problems, but the optimization is still more fragile than fitting a linear model or a convex regularized model.
MLPs are sensitive to preprocessing and hyperparameters. Features measured on very different scales can produce gradients with very different magnitudes, making a single learning rate awkward to choose. Training can stall or diverge. Hidden width, activation, learning rate, batch size, regularization strength, and training duration all affect the fitted model. A small network can underfit nonlinear structure; a large network can overfit, especially with few labeled examples.
Finally, MLPs are less interpretable than linear models. The weights of the first layer are not directly the effects of the original features, because they are mixed by the hidden units and the activation function. Interpretation usually requires post hoc methods, which we treat later in the book. For small tabular datasets where a linear model or a tree ensemble already performs well, the extra complexity of an MLP may not be justified.
Formula
With the motivation in place, the formula section answers a practical question: what exactly does the network compute, and how do we adjust every parameter so that its predictions improve? We will proceed in the same order as the implementation. First we define the forward pass and keep track of shapes. Then we define a numerically stable loss. Finally we differentiate that loss layer by layer. The order matters: backpropagation is not a separate learning algorithm, it is the chain rule applied to the forward computation we have just written down.
We begin with notation. Suppose we have training examples, each with input features. Let be the -th input and its binary label. We use a network with one hidden layer containing hidden units. The hidden layer has weight matrix and bias vector . The output layer has weight vector and bias .
where:
- : number of training examples, with one example per row when stored in the matrix
- : number of input features per example
- : the -th input vector, an element of
- : the binary label of the -th example, either or
- : number of hidden units in the single hidden layer
- : hidden-layer weight matrix, whose -th row holds the weights feeding hidden unit
- : hidden-layer bias vector, whose -th entry shifts hidden unit
- : output-layer weight vector, whose -th entry scales hidden activation in the output logit
- : output-layer bias, which shifts the logit before the sigmoid
The symbol means the set of real matrices with rows and columns. Each row of is the weight vector for one hidden unit. The bias shifts the -th hidden unit, just as the intercept shifts a regression line. One way to read the notation is as a contract: the input has numbers, the hidden layer is allowed to create new numbers, and the output layer must combine those numbers into one prediction. The rest of the derivation is about making that contract precise and keeping the dimensions consistent.
Affine layers, nonlinear activations, and shapes
We start with the simplest part: an affine transformation. For a single input , the hidden preactivation is
where:
- : a single input vector, so that applies the hidden weight matrix
- : the hidden preactivation vector, the affine output before the nonlinearity
The vector contains one number per hidden unit. The -th component is
where:
- : the preactivation of hidden unit
- : the weight connecting input feature to hidden unit , the entry in row and column of
- : the -th feature of the input
- : the bias of hidden unit
- : the number of input features
We read as “sum over the input features .” The entry is the weight connecting input feature to hidden unit . This is exactly the same weighted sum that appears in linear regression, repeated times with its own weights for each hidden unit. The hidden layer is a bank of learned affine projections, which can respond to different directions in the input space. The bias shifts the point at which the -th projection is centered.
Without a nonlinearity, stacking two affine layers would collapse into one affine layer:
which is still affine (linear plus a constant) in . A nonlinear activation allows the composition to represent functions that a single affine layer cannot. A common choice is the hyperbolic tangent,
where:
- : the activation of hidden unit
- : the preactivation of hidden unit entering the activation
The derivative of tanh is
Another common choice is the rectified linear unit,
where:
- : the activation of hidden unit
- : the preactivation of hidden unit entering the activation
The ReLU derivative is when and when . At exactly zero the derivative is not defined, and implementations choose a convention, usually . The sigmoid activation
where:
- : the logistic (sigmoid) function mapping any real input to
- : the input to the sigmoid, for example an output logit
The sigmoid is often used at the output for binary classification, and sometimes in hidden layers. Its derivative is . These derivatives tell us how a small change in a preactivation changes the activation. The backward pass multiplies them by the upstream loss gradient to obtain the gradient with respect to the preactivation. In a saturated region, tanh or sigmoid has a small derivative and attenuates a fixed upstream gradient. A ReLU has derivative zero on the negative side, so its local data-loss gradient is zero for that input.
The figure below makes the local exchange rates visible. Solid curves are the activations, and dashed curves are their derivatives. Where a derivative is near zero, the backward pass transmits very little gradient. The figure also shows that sigmoid and tanh flatten at large magnitudes, while ReLU stays linear on the positive side and becomes exactly zero on the negative side.
The hidden setup below makes the shared plotting module available and seeds the book plots once. Each plotting cell imports the style helpers it uses. The cell after it only prepares the data that the activation figure will display.
PythonIn [3]Expand
import numpy as np
z_grid = np.linspace(-4.0, 4.0, 401)
tanh_vals = np.tanh(z_grid)
sigmoid_vals = 1.0 / (1.0 + np.exp(-z_grid))
relu_vals = np.maximum(0.0, z_grid)
tanh_deriv = 1.0 - tanh_vals**2
sigmoid_deriv = sigmoid_vals * (1.0 - sigmoid_vals)
relu_deriv = (z_grid > 0.0).astype(float)VisualizationOut [4]Expand

For a mini-batch of examples, we store the inputs in a matrix , with one example per row, and use the same hidden-layer mapping as in the single-example derivation. The hidden preactivation matrix is
where:
- : the number of examples in the mini-batch
- : the input matrix, with one example per row
- : the transpose of the hidden weight matrix
- : a column vector of ones with length
- : the transpose of the hidden bias, broadcast across rows
- : the hidden preactivation matrix
The transpose appears because has examples as rows while has hidden units as rows. The product has shape : one row per example, one column per hidden unit. Broadcasting adds the same bias vector to every row. The activation matrix is , where is applied elementwise.
The output logit is
where:
- : the hidden activation matrix
- : the output weight vector
- : the output bias
- : the output logit vector of length
For binary classification we convert the logit to a probability with the sigmoid:
where:
- : the output logit of example
- : the predicted probability that example belongs to class
The shape chain is worth checking: is , is , and and are . In the column-vector notation used here, is and is . The NumPy implementation stores these vectors as one-dimensional arrays with shapes (h,) and (B,); the scalar formulas are the same. Writing each shape beside the corresponding operation helps find dimension mismatches before coding.
Forward passes and stable losses
The forward pass is the sequence of operations that turns inputs into predictions. For one mini-batch we compute , then , then , then . The forward pass stores , , and in a cache. This tanh backward pass reuses to compute the activation derivatives elementwise as ; it receives separately and does not read the cached . It is useful to think of the forward pass as building a cache: every quantity that will be needed later is stored now, so the backward pass can reuse it instead of recomputing it.
For binary labels, the negative log-likelihood of one example is
where:
- : the binary cross-entropy loss for example
- : the binary label, either or
- : the predicted probability of class for example
This is the binary cross-entropy loss. If , only remains; if , only remains. The loss is small when the predicted probability agrees with the label and large when it disagrees. In that sense the loss is a precise way of saying “how surprised should the model be by the observed label?”
For a very negative logit, a naive sigmoid calculation can overflow in and round the predicted probability to zero. Taking its logarithm then produces negative infinity even though the mathematical log probability is finite. We compute the loss from the logit instead. Since , we have
and
where:
- : the predicted probability for the example, equal to
- : the output logit for the example
Combining the two cases gives
where:
- : the binary cross-entropy loss for example , computed from the logit
- : the output logit of example
- : the binary label, either or
The expression is exactly , which is a log-sum-exp written in the form . Numerical libraries provide a stable function often called logaddexp, which computes without overflowing. Thus we compute
For label 1 the loss decreases as the logit grows; for label 0 it increases. The logaddexp calculation avoids forming an overflowing exponential directly. The plotted losses remain finite over the displayed logit range.
PythonIn [5]Expand
import numpy as np
s_curve = np.linspace(-5.0, 5.0, 401)
loss_label_one = np.logaddexp(0.0, s_curve) - 1.0 * s_curve
loss_label_zero = np.logaddexp(0.0, s_curve) - 0.0 * s_curveVisualizationOut [6]Expand

where:
- : a numerically stable computation of
- : the output logit of example
- : the binary label, either or
For a batch we average:
where:
- : the mean binary cross-entropy over the examples
- : the number of examples in the batch
- : the binary cross-entropy loss for example
The same idea extends to multiclass classification. If the network produces logits for classes, the softmax probability of class is
where:
- : the predicted probability that example belongs to class
- : the logit for class on example
- : the number of classes
For this multiclass formula, let denote the true class index, replacing the earlier binary encoding. The multiclass cross-entropy is
where:
- : the cross-entropy loss for example
- : the number of classes
- : the logit for class on example
- : the logit for the true class on example
The second term is computed with a stable log-sum-exp operation. In this chapter we focus on the binary case, but the backward pass for softmax cross-entropy has the same clean form: the gradient with respect to the logits is predicted probability minus one-hot label.
Backpropagation and gradient checking
Backpropagation is the chain rule applied from the output backward. We first compute the derivative of the loss with respect to the output logit . A useful discipline is to derive the gradient for one scalar example first, then average over the batch; the only change is the factor , which appears in the delta defined below. For one example,
where:
- : the binary cross-entropy loss for one example
- : the output logit for that example
- : the predicted probability for that example
We read as the partial derivative of the loss with respect to the logit . The derivative of the binary cross-entropy with respect to is
where:
- : the binary cross-entropy loss for one example
- : the predicted probability for that example
- : the binary label of that example
The derivative of the sigmoid is . Multiplying gives
where:
- : the derivative of the loss with respect to the logit
- : the predicted probability
- : the binary label
This is a simple result: the gradient at the logit is the predicted probability minus the label. If the network predicts and the label is , the gradient is , so gradient descent increases the logit slightly. If the label is , the gradient is , a larger correction. The same pattern will reappear in multiclass classification, where the gradient is the predicted probability vector minus the one-hot label.
For a batch with mean loss, we divide by . Define
where:
- : the scaled output delta for example
- : the predicted probability for example
- : the binary label for example
- : the number of examples in the batch, so division by reflects the mean loss
The gradient with respect to the output weights is
where:
- : the mean loss over the batch
- : the -th output weight
- : the activation of hidden unit on example
This holds because , so . In matrix form,
where:
- : the gradient of the mean loss with respect to the output weight vector
- : the hidden activation matrix
- : the vector of values
The transpose is needed because is and is , so is , matching . The gradient with respect to the output bias is
where:
- : the output bias
- : the scaled output delta for example
Now we propagate backward into the hidden layer. For hidden unit and example , the logit depends on through , so
where:
- : the derivative of the mean loss with respect to the activation of hidden unit on example
- : the scaled output delta for example
- : the output weight for hidden unit
The activation contributes its local derivative:
where:
- : the derivative of the mean loss with respect to the preactivation of hidden unit on example
- : the scaled output delta for example
- : the output weight for hidden unit
- : the derivative of the hidden activation evaluated at the preactivation
For , . Define the hidden delta matrix
where:
- : the hidden delta for hidden unit on example
- : the scaled output delta for example
- : the output weight for hidden unit
- : the activation of hidden unit on example
In matrix notation,
where:
- : the hidden delta matrix
- : the vector of scaled output deltas
- : the output weight vector, so is a outer product
- : the hidden activation matrix
- : elementwise (Hadamard) multiplication
The outer product has shape , and has the same shape. The hidden preactivation is , so
where:
- : the derivative of the mean loss with respect to the hidden weight connecting input feature to hidden unit
- : the derivative of the mean loss with respect to the hidden bias of unit
- : the hidden delta for hidden unit on example
- : the -th feature of example
In matrix form,
where:
- : the gradient of the mean loss with respect to the hidden weight matrix
- : the gradient of the mean loss with respect to the hidden bias vector
- : the transpose of the hidden delta matrix
- : the input matrix
- : the column vector of ones
Again the shapes match: is , is , so is , the same shape as . The gradient with respect to is the column sum of . Notice how each matrix expression encodes a sum: the transpose aligns examples with the quantity being summed, and the matrix product performs the sum over examples in one operation.
If we add L2 regularization, the loss becomes
where:
- : the regularized mean loss
- : the unregularized mean binary cross-entropy
- : the L2 regularization strength, with
- : the hidden weight connecting input feature to hidden unit
- : the output weight for hidden unit
The factor makes the derivative of the penalty with respect to equal to , and similarly for . We do not regularize the biases by default, because they do not control the scale of the learned features in the same way.
Gradient checking verifies the backward pass by comparing analytic gradients with finite differences. It is the unit test of a hand-written backward pass. For a parameter ,
where:
- : the -th scalar parameter being checked
- : the loss with parameter increased by the perturbation size
- : the loss with parameter decreased by
- : the finite-difference step size
Perturb one parameter by in each direction, compute the two losses, and divide their difference by . For a sufficiently smooth loss, the centered formula has truncation error of order . Floating-point cancellation grows as the step becomes too small, so this is not a guarantee of greater accuracy for every step size. A relative error below about is a useful diagnostic for a smooth float64 example at a suitable step size, but near-zero gradients and activation kinks need separate inspection.
Mathematical Properties
With a sufficiently wide hidden layer, a suitable continuous sigmoidal activation, and a linear output, a network can approximate continuous functions on a compact input domain to arbitrary accuracy. This is an existence statement about a family of functions, not a guarantee that gradient descent will find the required parameters from finite data. Our binary classifier instead uses a sigmoid output to keep predictions between zero and one. Approximation capacity does not remove the need for regularization and validation.
The joint loss is generally nonconvex in , , , and . It also has a permutation symmetry: permuting hidden units and applying the same permutation to rows of , entries of , and entries of leaves the network output unchanged. Distinct parameter vectors can therefore represent the same function. Random initialization helps give hidden units different starting points. If every weight and bias in this tanh network starts at zero, hidden activations and output weights are zero; the hidden features cannot begin learning under the stated gradient updates.
The gradient with respect to contains the input factor . Holding the other gradient factors fixed, rescaling that feature rescales this gradient. Standardization brings marginal feature scales closer together, but it does not remove correlations or guarantee a well-conditioned loss. Saturating tanh and sigmoid activations have small local derivatives at large magnitudes. These multipliers can attenuate gradient flow; ReLU avoids positive-side saturation but has zero derivative on its negative side.
Example
To make the formula concrete, we compute it by hand. The following example is deliberately tiny: two input features, two hidden units, and one binary output. Its purpose is not to be realistic but to expose every number that the code will later compute. We will carry the forward pass, the backward pass, and one gradient step by hand, so that each matrix in the implementation has a concrete scalar counterpart. Calculations retain full precision; displayed results are rounded to four decimal places.
Suppose we have two input features, two hidden units, and one binary output. Let the input be
Let the hidden weights and biases be
and let the output weights and bias be
We use in the hidden layer and sigmoid at the output.
Forward pass
We compute the hidden preactivations one unit at a time. For the first hidden unit,
Then
For the second hidden unit,
so
The output logit is
The predicted probability is
where:
- : the sigmoid function mapping the logit to a probability
- : the predicted probability that the example belongs to class
The loss is
where:
- : the binary cross-entropy loss for this single example
- : the natural logarithm
Equivalently, using the stable logit form,
We can read this forward pass as a pipeline: the input is projected into two hidden numbers, each hidden number is squashed by tanh, and the two squashed numbers are combined into a logit. The predicted probability is above , so the network already leans toward class , and the observed label is . Since the assigned probability is below , its binary cross-entropy is positive.
Backward pass
We now reverse the forward pass. The output gradient is
The output weight gradients are
where:
- : the output delta for this single example, without the scaling used for a batch mean
- : the hidden activations computed in the forward pass
- : the output weights
and the output bias gradient is
For the hidden units, we first compute
The tanh derivatives are
Therefore the hidden preactivation gradients are
Now we can compute the weight gradients. Since ,
Similarly,
The hidden bias gradients are simply and :
At this point every gradient has been computed by following the chain rule one link at a time. The output delta is the derivative of the loss with respect to the logit; its sign tells us which local change in the logit would reduce the loss. Multiplying by the hidden activations tells us how to change the output weights. Multiplying the output delta by the output weights and tanh derivative gives the loss gradient with respect to each hidden preactivation. Finally, multiplying by the input tells us how to change the hidden weights.
One gradient step
Gradient descent subtracts a multiple of the gradient. With learning rate , the new first hidden weight is
where:
- : the updated hidden weight after the gradient step
- : the initial value of
- : the learning rate
- : the gradient
The new first output weight is
where:
- : the updated first output weight after the gradient step
- : the initial value of
- : the learning rate
- : the rounded gradient
Applying the same rule at full precision and rounding the displayed values gives
A new forward pass with these updated parameters gives , , , , , , and loss . The loss decreased from about to about , which is the expected effect of a step in the negative gradient direction. It is a small confirmation that the signs and scales in the backward pass are consistent with the goal of reducing the loss.
Implementation in NumPy and Scikit-learn
We choose NumPy for the network because the goal is to expose every operation in the forward and backward passes. We use scikit-learn for data generation, train/test splitting, standardization, evaluation metrics, and the logistic regression baseline. The network itself is implemented with matrix operations and explicit gradients, so we can compare its predictions and check its gradients against finite differences. The point is not to hide the network behind a high-level API; it is to make the path from a mathematical expression to an executable update visible. All code cells below declare explicit Quarto directives so that computation is shown, while any printed results are displayed from calculated variables in a separate output cell.
We generate a noisy two-moons classification problem with examples, split it into training and test sets, and fit the scaler on the training set only. Fitting the scaler on the full dataset would leak test-set information into training, a point we emphasized in validation strategies and data leakage.
PythonIn [7]Expand
import numpy as np
from book_plot_style import PALETTE, polish_axes, theme_color
from sklearn.datasets import make_moons
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScalerPythonIn [8]Expand
X, y = make_moons(n_samples=600, noise=0.25, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.30, stratify=y, random_state=7
)
scaler = StandardScaler().fit(X_train)
Xtr = scaler.transform(X_train)
Xte = scaler.transform(X_test)
logit = LogisticRegression(max_iter=1000, random_state=0).fit(Xtr, y_train)The next cell defines the network functions. The forward pass returns the input, hidden preactivation, and hidden activation in its cache. This tanh backward pass retrieves the hidden activation and uses the separately passed input; it does not need the cached preactivation because each activation derivative is . The loss function uses the stable logit form. The backward pass implements the equations derived above, including the division by the batch size because the loss is a mean. As you read the code, match each line to an equation: delta_out is the output delta, delta_hidden is the hidden delta matrix, and the matrix products for v, W, b, and c are the gradient formulas written in the same order as the derivation.
PythonIn [9]Expand
def sigmoid(s):
return 1.0 / (1.0 + np.exp(-s))
def stable_binary_cross_entropy(s, y):
return np.mean(np.logaddexp(0.0, s) - y * s)
def init_params(n_in, n_hidden, rng):
W = rng.normal(0.0, np.sqrt(2.0 / n_in), size=(n_hidden, n_in))
b = np.zeros(n_hidden)
v = rng.normal(0.0, np.sqrt(2.0 / n_hidden), size=n_hidden)
c = np.array(0.0)
return {"W": W, "b": b, "v": v, "c": c}
def forward(params, X):
Z = X @ params["W"].T + params["b"]
A = np.tanh(Z)
s = A @ params["v"] + params["c"]
return s, {"X": X, "Z": Z, "A": A}
def loss_and_grads(params, X, y, lam=0.0):
s, cache = forward(params, X)
loss = stable_binary_cross_entropy(s, y)
loss += 0.5 * lam * (np.sum(params["W"] ** 2) + np.sum(params["v"] ** 2))
p = sigmoid(s)
delta_out = (p - y) / X.shape[0]
A = cache["A"]
grads = {
"v": A.T @ delta_out,
"c": np.sum(delta_out),
}
delta_hidden = np.outer(delta_out, params["v"]) * (1.0 - A**2)
grads["W"] = delta_hidden.T @ X
grads["b"] = np.sum(delta_hidden, axis=0)
grads["W"] += lam * params["W"]
grads["v"] += lam * params["v"]
return loss, gradsWe train with mini-batches. Each epoch shuffles the training examples, processes them in batches, and updates the parameters after each batch. Code and output are kept in separate cells: training runs in code-only cells, and the resulting metrics are displayed from calculated variables in output-only cells. At the end of every epoch we compute the full-batch training loss for monitoring. This is not used for the parameter updates, it is only a diagnostic. The monitoring loss is full-batch on purpose: it gives a stable view of the training objective even though the updates themselves are noisy.
PythonIn [10]Expand
def train_mlp(
X, y, n_hidden=16, epochs=400, batch_size=64, lr=0.08, lam=1e-3, seed=0
):
rng = np.random.default_rng(seed)
params = init_params(X.shape[1], n_hidden, rng)
n = X.shape[0]
history = []
for epoch in range(epochs):
order = rng.permutation(n)
for start in range(0, n, batch_size):
idx = order[start : start + batch_size]
_, grads = loss_and_grads(params, X[idx], y[idx], lam)
for key in params:
params[key] -= lr * grads[key]
history.append(loss_and_grads(params, X, y, lam)[0])
return params, np.asarray(history)Before trusting the training run, we check the analytic gradients against centered finite differences on a small network. The relative error should be small. The check is not a proof of correctness for every possible input, but it catches sign errors, missing terms, and shape mistakes that are common in hand-written backward passes. The check perturbs one parameter entry at a time, recomputes the loss, and compares the resulting slope with the analytic gradient for that same entry. A maximum relative error near machine precision confirms that the analytic and numerical gradients agree.
PythonIn [11]Expand
def numerical_gradient(params, X, y, lam=0.0, eps=1e-5):
num = {k: np.zeros_like(v) for k, v in params.items()}
for key in params:
it = np.nditer(
params[key], flags=["multi_index"], op_flags=["readwrite"]
)
while not it.finished:
idx = it.multi_index
original = params[key][idx]
params[key][idx] = original + eps
loss_plus, _ = loss_and_grads(params, X, y, lam)
params[key][idx] = original - eps
loss_minus, _ = loss_and_grads(params, X, y, lam)
params[key][idx] = original
num[key][idx] = (loss_plus - loss_minus) / (2.0 * eps)
it.iternext()
return num
rng_check = np.random.default_rng(11)
params_small = init_params(2, 3, rng_check)
X_small = rng_check.normal(size=(5, 2))
y_small = rng_check.integers(0, 2, size=5).astype(float)
loss_small, grads_small = loss_and_grads(
params_small, X_small, y_small, lam=0.01
)
num_small = numerical_gradient(params_small, X_small, y_small, lam=0.01)
max_rel_error = max(
np.max(
np.abs(grads_small[k] - num_small[k])
/ np.maximum(1e-8, np.abs(grads_small[k]) + np.abs(num_small[k]))
)
for k in grads_small
)With the analytic and numerical gradients computed, we can inspect their agreement directly. The plotting data below reuse the already-seeded small network from the gradient check; no new sampling or fitting is performed.
PythonIn [12]Expand
gradient_blocks = [
np.column_stack([grads_small[key].ravel(), num_small[key].ravel()])
for key in grads_small
]
grad_pairs = np.vstack(gradient_blocks)VisualizationOut [13]Expand

After checking the small-network derivatives, we fit the one-hidden-layer model on the training partition using the prespecified hyperparameters below.
PythonIn [14]Expand
params, history = train_mlp(
Xtr,
y_train.astype(float),
n_hidden=16,
epochs=400,
batch_size=64,
lr=0.08,
lam=1e-3,
seed=3,
)The next cell computes the test accuracy and log loss for both models. The final training loss and the maximum relative gradient-check error were computed in the earlier cells. The following cell then displays all of these results, which are calculated from the executed variables rather than hardcoded.
PythonIn [15]Expand
s_te, _ = forward(params, Xte)
p_mlp_te = sigmoid(s_te)
mlp_pred = (p_mlp_te >= 0.5).astype(int)
mlp_acc = accuracy_score(y_test, mlp_pred)
logit_acc = accuracy_score(y_test, logit.predict(Xte))
mlp_loss = stable_binary_cross_entropy(s_te, y_test.astype(float))
probs_logit = np.clip(logit.predict_proba(Xte)[:, 1], 1e-12, 1 - 1e-12)
logit_loss = -np.mean(
y_test * np.log(probs_logit) + (1 - y_test) * np.log(1 - probs_logit)
)The table compares both models on one held-out split. The full-batch training objective decreased during the displayed run, and the small relative gradient-check error shows agreement between the analytic and numerical derivatives on the checked inputs. Neither observation proves global convergence or better generalization. The decision-boundary figures below show the shape learned by each model; that graphical property is distinct from its test log loss.
The executable example uses fixed hyperparameters and does not implement validation-based selection. Its test data support the final metrics and illustrative plots. When tuning a model, reserve validation data from the training partition or use cross-validation, then evaluate the selected procedure on untouched test data.
Key Parameters
The hyperparameters are not independent knobs; they trade off capacity, optimization stability, and generalization. The main ones are:
n_hidden: the width of the hidden layer (default: 16). A larger value gives the network more capacity to represent complicated functions, but it also increases the number of parameters and can increase overfitting risk when the added capacity is not controlled by data and regularization. Start around 8 to 32 for low-dimensional tabular data.lr: the learning rate (default: 0.08). If it is too large, the loss can oscillate or diverge; if it is too small, training takes many epochs. Values around 0.01 to 0.1 are reasonable starting points for standardized inputs.batch_size: the maximum number of examples in each mini-batch gradient estimate (default: 64); the final batch may be smaller. At fixed parameters, smaller batches can give noisier gradient estimates. That randomness can sometimes move training out of a narrow basin, where the loss rises quickly around a local minimum, but it can also keep the objective from settling. Values around 32 to 128 are starting candidates for this small standardized example; choose among them with validation under a comparable training budget.epochs: the number of passes over the training data (default: 400). Choose this with early stopping on a validation split.lam: the L2 regularization strength (default: 1e-3). It shrinks the weights toward zero and can improve generalization, especially when the network is wide relative to the amount of training data. Values around 1e-4 to 1e-2 are reasonable starting ranges.seed: the seed for the random number generator (default: 0). Set it to an integer to ensure consistent initialization and mini-batch shuffling across runs.
The initialization scale is controlled inside init_params. We use a standard deviation proportional to for the first layer and for the output layer, which keeps the initial preactivations from growing too quickly with the number of inputs.
Key Methods
The implementation is deliberately small, and each function covers one conceptual step, described in its surrounding prose. The plain gradient descent loop in train_mlp makes the mechanics visible; an extended training loop may use an adaptive optimizer and validation-based early stopping. Early stopping can be added to either plain gradient descent or an adaptive optimizer.
Mini-batch training, regularization, and failure cases
At fixed parameters, averaging a uniformly sampled batch of 64 per-example gradients has no greater sampling variance than using one sampled example, and has lower variance in directions where the per-example gradients vary. During actual training the parameters change after each batch, so this fixed-parameter comparison does not guarantee a smoother or better optimization trajectory. For training examples and a positive integer maximum batch size , this loop makes updates per epoch. The ceiling rounds upward to an integer. Reducing cannot lower that count, but nearby batch sizes can give the same count. Mini-batch gradient noise can also keep the training objective from settling. Compare training and validation loss: decreasing training loss with rising validation loss suggests overfitting, while both remaining high suggests underfitting or an optimization problem.
L2 regularization adds a penalty proportional to squared weights. It discourages large weights, which can reduce sensitivity to individual training points. Dropout changes the forward pass during training and is outside this implementation. Early stopping selects training duration with validation data: retain the parameters from the best observed validation epoch and stop after a chosen period without improvement.
Hidden units with identical incoming weights, biases, and outgoing weights receive identical gradients under the stated updates, so they remain identical. Random initialization helps separate their starting points. Input scales can make different weight gradients very different in magnitude. A learning rate that is too high can make the loss explode; one that is too low can make progress hard to see. If a first-layer ReLU unit is negative on every training input, its data-loss gradient is zero. With plain gradient descent and no other parameter updates, such a unit cannot recover. Tanh and sigmoid can instead saturate at large preactivations, making their local derivatives or small.
Visualizing Multilayer Perceptrons
The training curve shows the regularized full-training-set objective after each mini-batch epoch. Since updates use different mini-batches, this monitored objective need not decrease at every epoch. The displayed decrease describes the optimization run; judging model capacity and stopping time also requires validation results.
VisualizationOut [17]Expand

The next two figures compare the decision boundaries of logistic regression and the trained MLP on the same standardized test data. The first shows the logistic boundary, which is constrained to be a straight line in the input space. The second shows the MLP, which can bend its boundary because the hidden layer transforms the inputs before the output layer makes its decision. On a two-moons problem, this flexibility is exactly what allows the network to represent a curved decision region.
The decision-boundary figures build a probability grid over the feature space for both models before plotting, so the plotting cells below contain presentation logic only. The logistic panel shows the strong linear constraint of logistic regression, while the MLP panel shows a curved boundary that can bend to follow the crescent shapes.
PythonIn [18]Expand
xx, yy = np.meshgrid(
np.linspace(Xte[:, 0].min() - 0.5, Xte[:, 0].max() + 0.5, 200),
np.linspace(Xte[:, 1].min() - 0.5, Xte[:, 1].max() + 0.5, 200),
)
grid = np.c_[xx.ravel(), yy.ravel()]
s_grid, _ = forward(params, grid)
p_mlp_grid = sigmoid(s_grid).reshape(xx.shape)
p_logit_grid = logit.predict_proba(grid)[:, 1].reshape(xx.shape)VisualizationOut [19]Expand

VisualizationOut [20]Expand

The comparison illustrates the role of the hidden layer. The MLP boundary bends to follow the two moons more closely than the linear baseline does, which is consistent with the hidden layer providing the representational flexibility to represent a curved region on this task. This graphical observation is distinct from the earlier single-split test metrics, and it does not by itself establish that the MLP has better generalization performance than the linear baseline. The logistic model has parameters and a linear log-odds surface. The MLP has parameters and can represent a much richer family of surfaces. That added capacity is not automatically better: with too little data, too much noise, or too little regularization, the MLP can fit idiosyncrasies of the training set. A validation split can guide capacity and regularization choices when tuning; that selection step is not implemented in this example, which uses prespecified hyperparameters.
Practical Implications
An MLP is a candidate for fixed-length numeric inputs when nonlinear relationships matter. Examples include tabular prediction with interactions, classification from fixed-length sensor or signal features, and learned feature stages inside larger systems. A one-hidden-layer network is a useful nonlinear baseline to validate before adding depth. Compare it with regularized logistic regression and tree ensembles using the same validation protocol. A simple linear model or shallow tree may be easier to inspect directly; large ensembles and MLPs can require additional explanation methods. Variance and tuning burden depend on the particular fitted models and regularization.
The practical question is not whether an MLP can represent a relationship, but whether the dataset, validation budget, and deployment constraints justify its extra capacity. A network that is too small underfits nonlinear structure, while a network that is too large can memorize noise. Comparing against a linear baseline and a tree ensemble clarifies whether the added flexibility produces a real gain. If the MLP does not improve validation performance after reasonable tuning, the simpler model is usually preferable.
Best Practices
Begin with a small one-hidden-layer network, for example n_hidden between 8 and 32 for low-dimensional tabular data, and increase width only if training and validation loss both remain high. For standardized inputs and mini-batch gradient descent with tanh activations, learning rates around 0.01 to 0.1, batch sizes around 32 to 128, and L2 strengths around 1e-4 to 1e-2 are reasonable initial ranges. These are starting points rather than universal settings. Evaluate several seeds because nonconvex training can produce noticeably different solutions.
Use a validation split to select the number of epochs and the regularization strength. Training loss alone is not a model-selection criterion. If training loss falls while validation loss rises, increase lam, reduce n_hidden, or stop earlier. If both losses remain high, increase capacity or train longer, but first check that the learning rate is not too small and that the inputs are properly prepared. Tune capacity and regularization before spending time on fine-grained learning-rate schedules.
Run a gradient check on a small network before a long training run. Compare analytic and numerical gradients for every parameter block, and treat a maximum relative error much larger than 1e-4 in float64 as a signal to inspect signs, missing terms, and shapes. After the check passes, monitor both training and validation curves, retain the parameters from the best validation epoch, and report the chosen threshold or decision rule separately from the fitted weights.
Data Requirements and Preprocessing
The network expects a fixed-length numeric matrix with one row per example. Categorical variables must be encoded, missing values must be imputed or handled explicitly, and any preprocessing that learns parameters, such as scaling, imputation, or target encoding, must be fitted on the training data only and then applied unchanged to validation and test data. Binary targets are encoded as 0 and 1; multiclass targets use a softmax output and an appropriate integer or one-hot representation; regression targets use a linear output and squared error.
For each example, the hidden-weight gradient contains the corresponding input value as a factor. Holding the other gradient factors fixed, rescaling that input rescales its gradient. Features with very different ranges can produce gradients of very different magnitudes and make a single learning rate ineffective. Standardizing each feature using training-set statistics is a sound default. If outliers are extreme, robust scaling or justified winsorization may be preferable, but the transformation must remain part of the deployed preprocessing pipeline.
The amount of data needed depends on the number of parameters, the noise level, and the complexity of the target function. A one-hidden-layer network has parameters, so increasing input dimension or hidden width adds parameters and can increase the data needed for reliable predictive performance. That effect depends on effective capacity, feature structure, noise, and regularization; parameter count alone gives no universal sample requirement. There is no universal sample-size threshold. Use validation performance and learning curves to decide whether more data, a smaller network, or stronger regularization is the appropriate response, and use stratified splits for classification when class frequencies are uneven.
Common Pitfalls
Initializing hidden units with identical incoming weights, biases, and outgoing weights preserves a symmetry under these gradient updates. The units receive identical gradients and cannot develop distinct features. Random initialization helps break that symmetry. A learning rate that is too large can also make the loss oscillate or grow, or move saturating activations into regions with very small derivatives. If the loss is unstable, inspect the learning rate before adding capacity.
Selecting a model by training loss can favor an overfit model. Training loss can improve while validation performance degrades, especially when the network is wide relative to the dataset. Keep a validation split, use early stopping, and tune regularization on validation data rather than on the test set. It is also easy to leak information by fitting a scaler, imputer, or feature selector on the full dataset before splitting; all such transformations belong inside a training-only pipeline.
Do not assume that 0.5 is the correct decision threshold for every binary problem. When classes are imbalanced or false positives and false negatives have different costs, the threshold should be chosen on validation data using the relevant metric. Finally, a passing gradient check does not guarantee good generalization; it only increases confidence that the backward pass implements the intended derivatives. Assessing model quality requires appropriate data preparation, validation, and comparison with simpler baselines.
Computational Considerations
For a mini-batch of examples, a forward pass through one hidden layer costs roughly operations, dominated by the input-to-hidden and hidden-to-output matrix products. The backward pass has the same order, and inference for a single example costs . Training therefore scales with the number of examples, the input dimension, the hidden width, and the number of epochs. Very wide hidden layers increase both computation and the number of parameters, while larger batches can improve hardware utilization until compute or memory limits are reached. Larger batches can also reduce the number of parameter updates per epoch; the count is , so nearby batch sizes can give the same count.
Batch activations and cached intermediates grow with the number of rows evaluated at once. Parameters and parameter gradients instead grow with input dimension and hidden width. This example forms full-training-set activations when it records the objective after each epoch, so its monitoring step is not strictly bounded by batch_size. For a large dataset, compute monitoring losses in chunks as well as training in mini-batches. Avoid retaining full-dataset activation matrices when they do not fit in memory.
Scalability also depends on preprocessing and evaluation. Transforming a large dataset repeatedly can dominate runtime if the scaler or feature pipeline is refitted unnecessarily. Fit preprocessing once on training data, cache the transformed arrays when memory permits, and evaluate on held-out data in batches. If training time becomes a constraint, reduce the number of epochs with early stopping, tune the batch size, or reduce the hidden width before adding more complex optimization machinery.
Performance and Deployment Considerations
Evaluate probabilistic classifiers with more than accuracy when probabilities matter. Log loss measures the quality of predicted probabilities, while accuracy, precision, recall, and ROC AUC emphasize different aspects of classification performance. For imbalanced data, precision-recall curves and the chosen decision threshold can be more informative than accuracy alone. Performance should be judged relative to a simple baseline on the same validation or test split; an MLP that matches a logistic regression model with much greater complexity may not be worth deploying.
A deployed model consists of more than its weight matrices. Save the fitted scaler or preprocessing object, the weight arrays, the architecture and activation choices, the threshold or decision rule, and the software versions used to produce them. Apply exactly the same preprocessing at inference time that was used during training. Monitor input distributions and predictive performance after deployment, because drift in features or class balance can degrade a model even when its code has not changed.
Use a held-out test set once for a final estimate, not for repeated tuning. Choose the stopping epoch and threshold on validation data, and report the corresponding test metrics with the preprocessing and evaluation protocol. For production systems, batch inference can improve throughput, while latency-sensitive applications may require a smaller network or additional optimization. Evidence of drift or degraded predictive performance should trigger investigation. Retrain when the change is relevant to the task and an updated model passes independent evaluation, including the appropriate gradient, validation, and baseline-comparison checks.
Summary
A multilayer perceptron composes affine transformations with nonlinear activations. The hidden layer creates a learned representation, and the output layer maps that representation to a prediction. For binary classification, a sigmoid output with binary cross-entropy gives a stable loss when written in terms of the logit as .
Backpropagation is the chain rule applied backward through the network. The gradient at the output logit is predicted probability minus label. The output weights receive the hidden activations multiplied by that gradient, and the hidden preactivations receive the output gradient multiplied by the output weights and the activation derivative. Matrix notation makes the implementation compact: matrix products express weighted sums, while outer and elementwise products combine local gradient factors.
We verified the gradients with finite differences, trained a one-hidden-layer network with mini-batches and L2 regularization, and compared its decision boundary with logistic regression. The MLP can represent curved boundaries that a linear model cannot, while introducing nonconvex optimization and additional architectural and training choices. Input scaling and initialization require care. The held-out comparison evaluates this fitted model on new examples, while the gradient check tests its derivative implementation.
Quiz
Check the affine-layer collapse, stable binary loss, and backpropagation updates.
Multilayer Perceptrons and Backpropagation
Join the discussionAsk a question or share what you learned.
Keep exploring
Reference
Citation details
Cite or share this article.

Comments
Add to the conversation
A question, a correction, or an example from your own work.
No comments yet. Be the first to share your thoughts!