Training neural networks well
You write a ten-layer network. The forward pass is right, every gradient passes a finite-difference check, the optimizer is AdamW with a sensible learning rate. And the loss sits at 2.30 for an hour, not moving. Or it moves for a while and then turns into NaN. Nothing is wrong with your math. Something is wrong with the numbers flowing through the network.
Training deep networks well is a craft, but it is a craft with reasons. The signals going forward and the gradients going backward are both products of many layers, and products of many numbers either explode or vanish unless something keeps them near 1. Initialization, normalization, the choice of activation, and residual connections (§0.8) are all ways of keeping them near 1. This section derives the first three, builds an MLP and both kinds of normalization from scratch with their backward passes, covers the classic regularizers, explains the surprising shape of the test-error curve called double descent, and ends with a debugging recipe that will save you days.
An MLP from scratch
A multilayer perceptron (MLP) alternates linear maps and elementwise nonlinearities. For a batch , a two-layer classifier with hidden width and classes computes
with , , an activation such as ReLU, and the mean softmax cross-entropy CE. In plain words, mix the inputs into new features, bend each feature with a nonlinearity, then mix those into class scores.
The backward pass is the chain of VJPs from §0.2, run from the loss back to the input.
where is the softmax of and the one-hot targets. Every line is either the linear-layer rule or the elementwise rule. For ReLU, is 1 where and 0 elsewhere, so the backward pass zeroes the gradient wherever the unit was off.
Why should such a simple model be able to fit anything? The universal approximation theorem (Cybenko, 1989, and Hornik, 1991) says that a single hidden layer with enough units can approximate any continuous function on a closed, bounded region as closely as you like. It is an existence result, though. It says nothing about how many units you need (possibly exponentially many), or whether gradient descent will find them. Depth buys enormous efficiency. Telgarsky (2016) constructed functions that a deep network represents with a modest number of units but that any shallow network needs exponentially many units to approximate. That is why we stack layers, and stacking layers is what creates the problems in the rest of this section.
A two-layer MLP, forward and backward
Implement mlp_loss_and_grads(X, y, params) for the network above with ReLU, where params is a dict with W1 (d, H), b1 (H,), W2 (H, K), b2 (K,), and y holds integer labels. Return (loss, grads) where loss is the mean cross-entropy and grads is a dict with the same keys and shapes as params. Make the softmax stable.
Activations
The nonlinearity is what makes depth worth anything, since a stack of linear layers is just one linear layer. But its derivative multiplies every backward signal passing through it, so its shape matters a great deal for training.
| activation | formula | derivative range | zero-centered | notes |
|---|---|---|---|---|
| sigmoid | no | saturates at both ends, and even at best shrinks gradients by 4 per layer | ||
| tanh | yes | saturates, but has slope 1 at 0, so it behaves linearly for small inputs | ||
| ReLU | no | no saturation for , cheap, sparse; units can die | ||
| Leaky ReLU | , small | roughly | a small slope for keeps dead units alive | |
| GELU | about | roughly | smooth ReLU; BERT and GPT-2 use it | |
| SiLU (Swish) | about | roughly | smooth; used inside SwiGLU in Llama-style models |
Here is the standard normal CDF, and GELU is often computed with the approximation . A few of these entries deserve a sentence each.
Sigmoid's derivative is at most 1/4. Backpropagating through ten sigmoid layers shrinks the gradient by at least , before even counting the weights. That alone made deep sigmoid networks nearly untrainable, and it is one reason ReLU took over around 2010 to 2012 (Glorot et al., 2011, and Krizhevsky et al., 2012).
ReLU units can die. If a unit's pre-activation is negative for every input (after a big update pushed its bias down, say), its gradient is zero everywhere and it can never recover. A large learning rate can kill a sizable fraction of a layer. Leaky ReLU and the smooth variants avoid this.
Smooth activations won in Transformers. GELU (Hendrycks & Gimpel, 2016) and SiLU are smooth near zero and have a small negative lobe. The gated feed-forward layers of modern LLMs, such as SwiGLU, multiply a SiLU branch by a linear branch, and they are covered in §5.3.
Initialization, derived
Before the first gradient step, the weights are random. How large should they be? This question decides whether a deep network can train at all, and the answer comes from the variance rules of §0.4.
First, a non-answer. Setting every weight to zero (or to the same constant) fails for a reason that has nothing to do with scale. Every hidden unit in a layer then computes the same function and receives the same gradient, so they stay identical forever, and the layer behaves like a single unit. Random initialization breaks the symmetry. The question is only how large the random values should be.
Keeping the forward signal steady
Consider one layer for a single input vector and , with weights drawn independently with mean 0 and variance , independent of , and no bias (biases start at zero). Each output is , a sum of uncorrelated terms (the weights are independent with mean zero, which makes the terms uncorrelated even when the inputs are not). Each term has mean and variance . Adding them up,
In plain words, each layer multiplies the signal's second moment by , times whatever the activation does. Over layers that factor is raised to the th power, so unless it is 1, the signal explodes or vanishes exponentially with depth.
For a linear or tanh network operating near zero, the input to the next layer has zero mean and , so the condition for preserving variance is
This is often called LeCun initialization (LeCun et al., 1998).
Keeping the backward signal steady
The backward pass sends , where each input gradient sums terms. The same calculation gives , so preserving gradient variance needs . Unless the layer is square, you cannot satisfy both. Glorot & Bengio (2010) proposed the compromise that averages the two fan counts,
If you draw from a uniform distribution instead, its variance is , so the same variance needs .
The ReLU correction
ReLU breaks one assumption above. Its output is never negative, so it is not zero-mean, and it zeroes half its inputs. Let be a pre-activation, symmetric around zero (as it is when the weights are), and the next layer's input. Then equals for the positive half and 0 for the negative half, and by symmetry
Plug this into the forward rule. The pre-activation variance of the next layer is , so preservation requires
In plain words, ReLU throws away half the signal's energy at every layer, so the weights must be times larger to compensate. He et al. (2015) derived this and used it to train 30-layer plain networks that Xavier initialization could not. They also showed that preserving the backward variance instead, , works just as well, because the two choices differ only by a ratio of layer widths that does not compound with depth.
The general pattern is , with a gain of 1 for linear layers, for ReLU, and for tanh in PyTorch's calculate_gain (a little above 1, to compensate for tanh squashing its larger inputs). Two practical notes. PyTorch's default nn.Linear initialization is uniform with bound , which gives variance , smaller than He, so deep ReLU networks often benefit from explicit Kaiming init. And Transformers usually use a small fixed standard deviation such as 0.02, with the output projections of residual branches scaled down by for layers (GPT-2 did this), so that the residual stream's variance does not grow with depth. That residual argument is the subject of §5.7, and the question of how initialization should change with width leads to muP in §9.5.
Watch the derivation come true. The widget pushes a batch through a deep stack of layers and plots the standard deviation of the activations at each layer, and of the gradients on the way back.
With ReLU, Xavier's variance is half too small, and after 30 layers the signal has shrunk by a factor of about , as the derivation predicts. He initialization keeps it near 1. With tanh, Xavier keeps the signal alive (it decays slowly, because tanh compresses large values), and a tiny constant scale kills it in a handful of layers.
Xavier and He initializers
Implement init_weights(fan_in, fan_out, scheme, dist, rng) returning a (fan_in, fan_out) weight matrix, where scheme is "xavier" (variance ) or "he" (variance ), and dist is "normal" or "uniform". For uniform, use the bound with equal to the target variance.
Vanishing and exploding gradients
The backward pass through layers multiplies the gradient by Jacobians, . Each Jacobian of a dense layer with an activation is . If these matrices typically shrink vectors by a factor (how much a matrix can stretch or shrink a vector is measured by its singular values, the subject of §0.10), the gradient at the first layer is about times the one at the last, and the early layers stop learning. That is the vanishing gradient problem. If they typically stretch by , gradients grow like , steps become huge, and the loss diverges. That is the exploding gradient problem.
Initialization makes at the start of training. It cannot guarantee it stays there, since the weights and activation statistics change as training proceeds. That is why deep networks also rely on normalization layers and residual connections, which keep the signal well scaled throughout training, not just at step zero. Recurrent networks are the extreme case, since they multiply by the same matrix at every time step, and §0.8 studies them.
Batch normalization
Batch normalization (Ioffe & Szegedy, 2015) takes a direct approach. If you want each layer's inputs to have zero mean and unit variance, normalize them to have exactly that, using statistics of the current minibatch. For a batch of activations ( examples, features), BatchNorm normalizes each feature (column) over the batch,
where are learned scale and shift parameters and (such as ) prevents division by zero. In plain words, standardize each feature across the batch, then let the network rescale and shift it however it likes. The learned and mean normalization never limits what the layer can represent. Setting and would undo it.
Train mode and eval mode
At test time you may see a single example, which has no batch statistics. So during training BatchNorm keeps exponential moving averages of and , and at evaluation time it normalizes with those fixed values instead. In PyTorch the update is with momentum , and the running variance uses the unbiased batch variance (dividing by , as in §0.4), while the normalization itself uses the biased one. This train-eval split is the reason model.eval() exists, and forgetting it is one of the most common PyTorch bugs (§0.9).
The backward pass
This is a classic interview derivation, and it rewards care, because every output in a column depends on every input in that column through and . Work with one feature column and drop the index . Write , and let be the upstream gradient for that column. The parameter gradients are immediate, and . For the input, the result is
In plain words, take the upstream gradient, remove its mean, remove its component along the normalized inputs, and scale by . Those two removals are the gradient's way of respecting normalization's invariances. Shifting all inputs by a constant, or scaling them, does not change , so the gradient has no component in those directions.
∂ Deriving the BatchNorm backward pass
Let . We need the Jacobian . Three pieces.
The mean gives .
The variance is . Differentiating, , and the second term is zero because deviations from the mean sum to zero. So , and .
Now differentiate with the quotient rule,
Finally apply the chain rule, summing over every that affects,
Substituting gives the boxed formula. Computing it this way costs per feature, the same as the forward pass, with no Jacobian.
Why BatchNorm helps, and its problems
Networks with BatchNorm train much faster, tolerate learning rates several times larger, and are far less sensitive to initialization. Why is debated. The original paper attributed it to reducing "internal covariate shift", the changing distribution of each layer's inputs during training. Santurkar et al. (2018) showed experimentally that deliberately injecting noise after BatchNorm, to increase that shift again, barely hurts, and argued that the real benefit is a smoother loss landscape with better-behaved gradients. Practitioners agree that it works more than they agree on why.
It also has real drawbacks. Its output for one example depends on the other examples in the batch, which couples examples, breaks down for small batches (the statistics get noisy), and complicates distributed training, where each device sees a different sub-batch. The train-eval discrepancy causes bugs. And for sequences of varying length, "the batch statistics of position 17" mixes padding with real tokens. These problems are why sequence models moved to the next layer.
BatchNorm, forward and backward
Implement batchnorm_forward(X, gamma, beta, eps=1e-5) returning (Y, cache) in training mode, and batchnorm_backward(dY, cache) returning (dX, dgamma, dbeta). X has shape (N, D), and normalization is over axis 0. The backward pass must be vectorized and must not build an (N, N) Jacobian.
BatchNorm's running statistics and eval mode
Implement a small class BatchNorm1d(D, momentum=0.1, eps=1e-5) with attributes gamma, beta (initialized to ones and zeros), running_mean (zeros), running_var (ones), momentum, eps, and training (True), and a method __call__(X). In training mode it normalizes with batch statistics (biased variance) and updates the running statistics PyTorch's way, using the unbiased batch variance for running_var. In eval mode (training = False) it normalizes with the running statistics and changes nothing.
Layer normalization
Layer normalization (Ba, Kiros & Hinton, 2016) normalizes each example over its own features instead of each feature over the batch. For one token's vector ,
In plain words, standardize the features of each example on their own, then rescale per feature. Because nothing depends on other examples, LayerNorm behaves identically in training and evaluation, works with a batch of one, and handles variable-length sequences without caring about padding. It is the normalization of the Transformer, applied to every token's vector independently.
Its backward pass is the BatchNorm formula with the roles of the axes swapped. For one row, let and use that row's own mean and variance. The averages then run over the features of the row instead of the rows of a column, and since is per feature it sits inside the sums,
A cheaper variant, RMSNorm (Zhang & Sennrich, 2019), skips the mean subtraction and divides by the root mean square, . Most recent LLMs use it. Where to put the normalization (before or after each sublayer) matters enormously for training stability, which §5.2 explains.
| BatchNorm | LayerNorm | RMSNorm | |
|---|---|---|---|
| statistics over | the batch, per feature | the features, per example | the features, per example |
| depends on other examples | yes | no | no |
| train and eval differ | yes (running statistics) | no | no |
| subtracts the mean | yes | yes | no |
| typical home | CNNs for vision | Transformers, RNNs | modern LLMs |
LayerNorm, forward and backward, for any batch shape
Implement layernorm_forward(X, gamma, beta, eps=1e-5) returning (Y, cache) and layernorm_backward(dY, cache) returning (dX, dgamma, dbeta). X has shape (..., D) (for example (B, T, D)), normalization is over the last axis, and gamma, beta have shape (D,). dgamma and dbeta sum over all leading axes.
Dropout
Dropout (Srivastava et al., 2014) randomly zeroes each activation with probability during training. A network can then never rely on any single unit, since it might vanish. This discourages fragile co-adaptations between units and acts like training a huge ensemble of thinned networks that share weights.
The detail that matters is the scaling. In inverted dropout, which every framework uses, the surviving activations are divided by during training,
In plain words, keep each unit with probability and scale up the survivors so the expected activation is unchanged. At evaluation time dropout does nothing at all, and because expectations already match, no rescaling is needed. Dropout was essential for the large fully connected layers of 2012-era networks. In LLM pretraining it is commonly set to zero, since a model that sees each token roughly once has little opportunity to overfit, while fine-tuning on small data sets often turns it back on.
Inverted dropout
Implement dropout(x, p, training, rng) returning (out, mask). In training mode, mask is a boolean array of the same shape as x that is True with probability 1 - p, and out = x * mask / (1 - p). In eval mode, return x unchanged and a mask of all True. Handle p = 0.
Weight decay
Weight decay adds to the loss, where is all the weights and the decay strength. §0.4 showed this is MAP estimation under a Gaussian prior, and with SGD at learning rate it shrinks each weight by a factor per step. With Adam, use the decoupled AdamW form from §0.6. A typical value for Transformers is , applied to weight matrices but not to biases or normalization gains.
Overfitting, bias, variance, and double descent
A model overfits when it fits its training data much better than new data, learning the noise along with the signal. The classic way to reason about this is the bias-variance decomposition. Suppose with noise of mean 0 and variance , and a learning procedure produces from a random training set . At a fixed test point, write for the average prediction. Then
∂ Proving the decomposition
Write , and expand the square. The three squared terms give noise, bias squared, and variance. Each cross term vanishes. because the test noise has mean zero and is independent of the training set. because is the mean of .
In plain words, test error is irreducible noise, plus how wrong the model is on average (bias), plus how much it changes with the particular training set (variance). The classical picture says simple models have high bias and low variance, flexible ones the reverse, and the best model sits at a sweet spot in between, giving a U-shaped test-error curve. Regularization (weight decay, dropout, early stopping, data augmentation) trades a little bias for less variance.
Modern deep learning broke that picture. Networks with far more parameters than training examples fit their training data perfectly, yet generalize well. Belkin et al. (2019) showed the curve has a second part. Test error rises toward a peak at the interpolation threshold, where the model has just enough capacity to fit the training set exactly, and then falls again as capacity keeps growing. This is double descent. Nakkiran et al. (2019) found it in deep networks as a function of width, training time, and data set size.
The cell reproduces it with the simplest possible model, linear regression on random ReLU features, fit by the minimum-norm least-squares solution (which is what gradient descent from zero converges to). With 50 training points, watch what happens near 50 features.
Below 50 features, the classical U appears. At 50, the model must thread every noisy training point with a single exact solution, which needs huge coefficients, and test error spikes. Beyond 50 there are infinitely many exact fits, and the minimum-norm one gets smoother and better as features are added, until it beats every model on the classical side. The deep-learning lesson is that "more parameters than data" is not by itself a reason to expect overfitting. The implicit regularization of the training procedure matters as much as the parameter count.
A recipe for debugging a training run
When a network will not train, the bug is almost never where you first look. Karpathy's "A Recipe for Training Neural Networks" (linked below) captures the habits that find it fast. Here is a condensed version, in the order to apply it.
- Check the loss at initialization. With balanced classes and a well-initialized output layer, the logits are near zero, the softmax is near uniform, and the loss should be (2.303 for 10 classes, and 10.82 for a language model with GPT-2's 50,257-token vocabulary, as in §0.5). A much larger initial loss means the outputs are overconfident from the start, usually because the last layer's weights are too large.
- Overfit a single small batch. Turn off regularization and train on one batch of, say, 32 examples. The loss should go to nearly zero within a few hundred steps. If it does not, there is a bug in the model, the loss, or the data pipeline, and no hyperparameter will fix it.
- Verify the data. Look at actual inputs and labels right before they enter the model. Check shapes, value ranges, and that labels line up with inputs after shuffling and augmentation.
- Start simple and add one thing at a time. A small model with a known-good setup first. Then add complexity, checking that each change helps.
- Watch the right signals. Training and validation loss, the gradient norm, the update-to-weight ratio (around per step is a common rule of thumb), and activation statistics per layer. Saturated or dead units show up there long before the loss tells you.
- Tune the learning rate first. It is the most important hyperparameter. Sweep it on a log scale.
- Regularize only once you overfit. Get the training loss down, then trade some of it for validation performance. And before you believe a small validation improvement, check that it is larger than the run-to-run noise across seeds (§0.11 shows how).
With a large output-layer scale the initial loss is far above , a sign of overconfident random predictions that waste the first part of training undoing them. With a small scale it starts at as it should. Either way the network memorizes a batch of random labels, which confirms that the forward and backward passes are wired correctly. If this check failed, nothing else would matter.
In PyTorch
Read more
- Glorot & Bengio (2010). Understanding the difficulty of training deep feedforward neural networks and He, Zhang, Ren & Sun (2015), Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification. The variance analyses behind Xavier and He initialization (the ReLU correction is in He et al., Section 2.2).
- Telgarsky (2016). Benefits of depth in neural networks. A clean proof that some functions need exponentially more units when the network is shallow.
- Ioffe & Szegedy (2015). Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift. The original method, including inference with population statistics.
- Ba, Kiros & Hinton (2016). Layer Normalization. Per-example normalization, designed for recurrent networks.
- Santurkar, Tsipras, Ilyas & Madry (2018). How Does Batch Normalization Help Optimization? Evidence against the covariate-shift story, and the smoothness explanation.
- Srivastava et al. (2014). Dropout: A Simple Way to Prevent Neural Networks from Overfitting. The method and its ensemble interpretation.
- Belkin, Hsu, Ma & Mandal (2019). Reconciling modern machine learning practice and the bias-variance trade-off and Nakkiran et al. (2019), Deep Double Descent: Where Bigger Models and More Data Hurt. The double-descent curve, in simple models and in deep networks.
- Andrej Karpathy (2019). A Recipe for Training Neural Networks. The debugging habits this section condenses.