- Derive Xavier and He initialization from the goal of preserving activation variance across layers
- Explain why gradients vanish or explode in deep networks and how normalization and residual connections fix it
- Compute batch normalization and layer normalization by hand and choose between them
- Configure AdamW, warmup with cosine decay, gradient clipping and early stopping, and explain each one's role
- Use FP16 and BF16 mixed precision correctly, including when loss scaling is required
- Debug a failing training run using the overfit-one-batch test, initial-loss checks and loss-curve diagnosis
Before this: Neural Networks and Backpropagation, Optimization for Machine Learning, Model Evaluation and Metrics
Knowing backpropagation is not the same as being able to train a deep network. Stack twenty layers naively and the loss may sit flat, explode to NaN, or drop nicely and then the model fails on validation data. The techniques in this lesson, mostly developed between 2010 and 2017, are what turned "deep learning" from a fragile research craft into a reliable engineering discipline, and every one of them is still in daily use in training everything from small classifiers to large language models. The final section gives a training-loop template you can reuse.
Intuition: a signal passing through many hands
Think of a deep network as a long chain of people passing a message. If each person whispers a little more quietly than they heard, by the twentieth person the message is inaudible; if each one shouts a little louder, it becomes a deafening roar. The same happens to activations on the forward pass and to gradients on the backward pass, because each layer multiplies the signal by its weights and activation derivatives. Training deep networks is largely about keeping that signal at a sensible volume all the way through: good initialization sets the starting volume, normalization re-levels it at every layer, residual connections add a direct line that bypasses the chain, and gradient clipping caps the occasional shout.
Weight initialization
Why scale matters
Consider a linear layer with independent, zero-mean weights and inputs. Then
If , variance grows layer after layer (exploding); if it is , it shrinks (vanishing). Through layers the factor compounds as . With and weights drawn with standard deviation 1, the factor per layer is 512, so activation variance passes FP16's maximum within two layers and FP32's within about fifteen.
Xavier (Glorot) and He initialization
- Xavier/Glorot (Glorot and Bengio, 2010) balances the forward condition and the backward condition by compromise: . As a uniform distribution, with . Derived for symmetric, roughly linear-near-zero activations such as tanh.
- He/Kaiming (He et al., 2015) accounts for ReLU zeroing half its inputs, which halves the second moment of the activations: . Use it for ReLU-family networks.
Worked numbers. A ReLU layer with : He standard deviation . A tanh layer mapping 512 to 256 units: Xavier uniform bound .
Biases are usually initialized to zero. PyTorch's nn.Linear uses a Kaiming-uniform-style default scaled for general use; torch.nn.init.kaiming_normal_ and xavier_uniform_ let you choose explicitly. Transformers commonly use small normal initializations (standard deviation around 0.02) with scaled-down initialization for residual output projections, alongside normalization layers.
Vanishing and exploding gradients
By the chain rule, the gradient at layer 1 of an -layer network contains a product of Jacobians, each including the weight matrix and the activation derivative. If their typical magnitude is below 1, the product vanishes exponentially; above 1, it explodes. The sigmoid's derivative is at most 0.25, so ten sigmoid layers can scale gradients by as little as even before weights are considered. Early layers then barely learn, which is why sigmoid hidden layers gave way to ReLU-family activations.
Symptoms. Vanishing: loss plateaus early, gradient norms of early layers are orders of magnitude smaller than late ones. Exploding: loss spikes or becomes NaN/inf, gradient norms jump. Recurrent networks, which apply the same weights at every time step, are especially prone (see the sequences lesson).
Remedies, in order of importance: ReLU/GELU activations, proper initialization, normalization layers, residual connections, gradient clipping, and a sensible learning rate.
Normalization layers
Batch normalization
Batch normalization (Ioffe and Szegedy, 2015) normalizes each feature using statistics of the current mini-batch :
with learnable scale and shift (so the network can undo the normalization if useful) and a small for numerical stability. During training it also keeps running averages of and , which are used at inference instead of batch statistics.
Worked example. One feature across a batch of four: . . , . Normalized (ignoring ): . With , : .
Batch norm allows higher learning rates and made very deep CNNs practical. The original paper motivated it by reducing "internal covariate shift"; later work argues its main benefit is smoothing the optimization landscape, and the precise mechanism is still debated. Its weaknesses come from depending on the batch: it behaves poorly with small batches, differs between training and inference modes (forgetting model.eval() is a classic bug), and is awkward for variable-length sequences.
Layer normalization
Layer normalization (Ba, Kiros and Hinton, 2016) computes and over the features of a single example instead of over the batch, then applies the same affine step. It behaves identically in training and inference and is independent of batch size, which is why it is standard in Transformers and RNNs. RMSNorm, a simplification that skips mean-centering and divides by the root-mean-square, is common in recent large language models. Rule of thumb: batch norm for CNNs with reasonable batch sizes, layer norm (or RMSNorm) for Transformers and sequence models.
Residual connections
A residual block (He et al., 2015, ResNet) computes , where is a small stack of layers. The layers learn a correction to the identity rather than a whole new mapping. Two effects follow. First, if extra layers are not useful they can learn , so adding depth should not hurt. Second, the backward pass gets a direct path: , so gradients can flow through the identity term even if is small. Residual connections enabled networks with over 100 layers and are in every Transformer block. Modern Transformers typically use pre-norm blocks, , which tend to train more stably than placing the normalization after the addition.
Regularization: dropout and weight decay
Dropout (Srivastava et al., 2014) randomly zeros each activation with probability during training. Implementations use inverted dropout, scaling surviving activations by so the expected value is unchanged and nothing needs rescaling at test time. With , kept activations are doubled. Dropout discourages co-adapted features and acts like an ensemble of thinned networks. It is switched off by model.eval(). Typical rates: 0.1 to 0.5 in MLPs; around 0.1 or none in large Transformers trained on huge datasets, where overfitting is less of a concern.
Weight decay shrinks weights toward zero each step, discouraging large weights, discussed with AdamW below. Other regularizers include data augmentation (often the most effective for vision and audio), label smoothing and early stopping.
Optimizers in practice: AdamW
Adam keeps exponential moving averages of the gradient and its square, with bias correction for their zero initialization:
Here is the learning rate, are decay rates (defaults 0.9 and 0.999), a small constant, and the weight-decay coefficient. The term written separately like this is the decoupled weight decay of AdamW (Loshchilov and Hutter). In plain Adam, weight decay is implemented by adding to the gradient, which then gets divided by , so parameters with large gradient history are regularized less than intended. AdamW applies decay directly to the weights, which generalizes better and makes easier to tune; it is the default optimizer for Transformers.
Worked step. On step 1, and , so the Adam part of the update is . With , a parameter whose gradient is 0.02 and one whose gradient is 50 both move by about . Adam's steps are roughly scale-free per parameter, which is why one learning rate works across layers with very different gradient magnitudes.
Practical defaults: AdamW with around for small models and to for Transformers, around 0.01 to 0.1, and no weight decay on biases and normalization parameters. SGD with momentum (0.9) and a tuned schedule remains competitive for CNNs and sometimes generalizes slightly better.
Learning-rate schedules
The learning rate is the single most important hyperparameter. Schedules change it during training:
- Warmup: increase linearly from near zero over the first few hundred to few thousand steps. Early on, Adam's second-moment estimates are noisy and the weights are random, so large steps can destabilize training; warmup is essential for Transformers.
- Cosine decay: after warmup, follow where is the number of warmup steps and the total.
- Step decay (divide by 10 at fixed epochs), linear decay, one-cycle, and reduce-on-plateau are common alternatives.
Worked example. , , , . At step 500 (warmup): . At step 3,250: progress , so . At step 5,500: progress 0.5, . At step 10,000: .
To find a starting , run a short learning-rate range test: increase exponentially over a few hundred steps and pick a value somewhat below where the loss starts to rise sharply.
Early stopping
Track validation loss (or the metric you care about) every epoch, save a checkpoint whenever it improves, and stop when it has not improved for a set number of epochs (the patience, e.g. 5 to 10). Then restore the best checkpoint. Early stopping is a cheap, effective regularizer that limits how far the weights move from initialization. Use a validation set separate from the final test set.
Mixed precision: FP16 and BF16
Training in 16-bit floating point halves memory for activations and uses the much faster low-precision units on modern accelerators.
| Format | Sign / exponent / mantissa bits | Largest value | Notes |
|---|---|---|---|
| FP32 | 1 / 8 / 23 | about | reference precision |
| FP16 | 1 / 5 / 10 | 65,504 | small range: small gradients underflow to zero |
| BF16 | 1 / 8 / 7 | about | FP32's range, less precision |
Mixed precision (Micikevicius et al., 2017) runs most matrix multiplications in 16-bit while keeping an FP32 master copy of the weights and doing sensitive operations (reductions, softmax, loss) in FP32. In PyTorch, torch.autocast chooses the precision per operation automatically.
With FP16, gradients smaller than about (the smallest subnormal) flush to zero, so you need loss scaling: multiply the loss by a large factor before backward(), so gradients are scaled into representable range, then divide by before the optimizer step. A dynamic scaler (GradScaler) raises while things are fine and lowers it, skipping the step, when it detects inf/NaN. BF16 has FP32's exponent range, so loss scaling is normally unnecessary; it is the default choice on hardware that supports it (NVIDIA GPUs from the Ampere generation onward, Google TPUs and others). Precision loss in BF16 (8 significant bits, about 2 to 3 significant decimal digits) is usually harmless for training because the master weights and accumulations stay in FP32.
Gradient clipping
Clip by global norm: if , rescale . This preserves the gradient's direction and caps its size, preventing a single bad batch from destroying training. Example: has norm 13; with it becomes . A max norm of 1.0 is a common default for Transformers. With FP16 loss scaling, unscale gradients before clipping, or the threshold is applied to scaled values. Logging the pre-clip gradient norm is one of the best health signals you have: steady growth or frequent spikes indicate instability.
Debugging training
A systematic order of checks, following the spirit of Karpathy's widely cited "recipe":
- Check the initial loss. With balanced classes and a properly initialized softmax classifier, the first loss should be about (2.303 for 10 classes). Much larger means the output layer is badly scaled or labels are wrong.
- Overfit a single batch. Take one batch of, say, 32 examples and train on it repeatedly. A correct model and pipeline drives loss to near zero within a few hundred steps. If it cannot, there is a bug (wrong labels, detached graph, missing
zero_grad(), frozen parameters, a learning rate far off), and no amount of data will fix it. - Visualize inputs right before they enter the model, after augmentation and preprocessing. Many bugs are in the data.
- Read the loss curves (training and validation on the same plot):
- Training loss flat from the start: learning rate too low, vanishing gradients, or a bug.
- Training loss explodes or goes
NaN: learning rate too high, no clipping, FP16 overflow, or a log of zero. - Training loss falls, validation loss rises after a point: overfitting; add regularization, data or augmentation, or stop earlier.
- Both high and close together: underfitting; increase capacity or train longer.
- Noisy, spiky training loss: batch too small or learning rate too high.
- Change one thing at a time and keep a log of experiments (see the MLOps lessons).
A PyTorch training-loop template
import math, torch
from torch import nn
def make_optimizer(model, lr=3e-4, wd=0.05):
decay, no_decay = [], []
for name, p in model.named_parameters():
if p.requires_grad:
(no_decay if p.ndim < 2 else decay).append(p) # no decay on biases/norms
return torch.optim.AdamW([{"params": decay, "weight_decay": wd},
{"params": no_decay, "weight_decay": 0.0}], lr=lr)
def warmup_cosine(opt, warmup, total):
def f(step):
if step < warmup:
return (step + 1) / warmup
progress = (step - warmup) / max(1, total - warmup)
return 0.5 * (1 + math.cos(math.pi * min(1.0, progress)))
return torch.optim.lr_scheduler.LambdaLR(opt, f)
def train(model, train_loader, val_loader, loss_fn, epochs=20, patience=5,
device="cuda", amp_dtype=torch.bfloat16, max_norm=1.0):
model.to(device)
opt = make_optimizer(model)
sched = warmup_cosine(opt, warmup=500, total=epochs * len(train_loader))
use_scaler = amp_dtype == torch.float16 # loss scaling only for FP16
scaler = torch.amp.GradScaler(device, enabled=use_scaler) # older: torch.cuda.amp
best, bad_epochs = float("inf"), 0
for epoch in range(epochs):
model.train()
for xb, yb in train_loader:
xb, yb = xb.to(device), yb.to(device)
with torch.autocast(device_type=device, dtype=amp_dtype):
loss = loss_fn(model(xb), yb)
opt.zero_grad(set_to_none=True)
scaler.scale(loss).backward()
scaler.unscale_(opt) # clip true gradients
nn.utils.clip_grad_norm_(model.parameters(), max_norm)
scaler.step(opt); scaler.update(); sched.step()
model.eval(); total, n = 0.0, 0
with torch.no_grad(), torch.autocast(device_type=device, dtype=amp_dtype):
for xb, yb in val_loader:
xb, yb = xb.to(device), yb.to(device)
total += loss_fn(model(xb), yb).item() * len(xb); n += len(xb)
val = total / n
print(f"epoch {epoch} val_loss {val:.4f} lr {sched.get_last_lr()[0]:.2e}")
if val < best:
best, bad_epochs = val, 0
torch.save(model.state_dict(), "best.pt") # checkpoint the best model
elif (bad_epochs := bad_epochs + 1) >= patience:
break # early stopping
model.load_state_dict(torch.load("best.pt"))
return model
What each piece does: parameter groups exclude biases and normalization weights from decay; LambdaLR multiplies the base learning rate by the warmup-cosine factor every step; autocast runs the forward pass in BF16 or FP16; the scaler is a pass-through when disabled (BF16) and handles loss scaling for FP16; gradients are unscaled before clipping; model.eval() and torch.no_grad() switch off dropout, use batch-norm running statistics and skip graph building during validation; the best checkpoint is saved and restored. On a CPU, set device="cpu" (CPU autocast supports BF16). The GradScaler constructor taking a device argument is the current API; older PyTorch versions use torch.cuda.amp.GradScaler().
Common pitfalls
- Forgetting
model.train()/model.eval(), so dropout stays on at evaluation or batch norm uses batch statistics at inference. - No warmup for Transformers, or a peak learning rate copied from a different model size or batch size.
- Using Adam with L2 regularization when you meant AdamW decoupled decay; and decaying biases and norm parameters.
- FP16 without loss scaling, or clipping before unscaling.
- Batch norm with tiny batches (for example 2 per GPU): statistics are too noisy; use group norm or layer norm, or synchronized batch norm.
- Judging by training loss alone; always plot validation loss on the same axes.
- Tuning many things at once without logging, making it impossible to know what helped.
In practice
- Default recipe for a new model: He or framework-default initialization, normalization layers, residual connections if deeper than a few layers, AdamW with warmup plus cosine decay, gradient clipping at 1.0, BF16 autocast where supported, early stopping on validation loss.
- Always run the overfit-one-batch test before a long run; it takes a minute and catches most bugs.
- Log training loss, validation loss, learning rate and gradient norm every few steps to an experiment tracker.
- When scaling batch size up, scale the learning rate up too (roughly linearly for SGD, more gently for Adam) and lengthen warmup, then re-verify.
- Fine-tuning pretrained models usually needs a learning rate 10 to 100 times smaller than training from scratch, short warmup, and few epochs.
Key takeaways
- Xavier and He initialization keep activation and gradient variance roughly constant across layers, with He's factor of 2 compensating for ReLU zeroing half its inputs.
- Vanishing and exploding gradients come from multiplying many layer Jacobians; ReLU-family activations, normalization and residual connections are the main cures.
- Batch norm normalizes over the batch and suits CNNs, while layer norm normalizes within each example and is standard in Transformers.
- AdamW decouples weight decay from the adaptive gradient step and, with warmup plus cosine decay and gradient clipping, is the default recipe for modern networks.
- BF16 mixed precision has FP32's range and usually needs no loss scaling, whereas FP16 needs dynamic loss scaling to avoid gradient underflow.
- Before any long run, check the initial loss and confirm the model can overfit a single batch; then diagnose with training and validation loss curves.
Terms in this lesson
- He initialization
- Drawing weights with variance 2 / fan-in so that ReLU networks preserve activation variance across layers.
- Vanishing gradient
- Gradients shrinking exponentially as they propagate backwards through many layers, so early layers barely learn.
- Batch normalization
- Normalizing each feature with the mean and variance of the current mini-batch, followed by a learnable scale and shift.
- Layer normalization
- Normalizing across the features of each individual example, independent of batch size and identical in training and inference.
- Residual connection
- A skip path that adds a block's input to its output, y = x + F(x), giving gradients a direct route backwards.
- Inverted dropout
- Zeroing activations with probability p during training and scaling survivors by 1/(1-p) so no rescaling is needed at inference.
- AdamW
- The Adam optimizer with weight decay applied directly to the weights rather than added to the gradient.
- Warmup
- Gradually increasing the learning rate from near zero over the first training steps to avoid early instability.
- Loss scaling
- Multiplying the loss by a large factor before backpropagation in FP16 training so small gradients do not underflow, then dividing it out before the update.
- Gradient clipping
- Rescaling the gradient when its global norm exceeds a threshold, preserving direction while capping step size.
Check your understanding
- 1.
Using He initialization, what standard deviation should the weights of a ReLU layer with 128 inputs have?
- 2.
You are training a Transformer with a per-GPU batch of 2 sequences of varying length. Which normalization is the best fit?
- 3.
Why does AdamW decouple weight decay from the gradient update?
- 4.
A warmup-cosine schedule has peak learning rate 1e-3, minimum 0, 1,000 warmup steps and 11,000 total steps. What is the learning rate at step 6,000?
- 5.
Your new model cannot drive the loss on a single batch of 16 examples below its starting value after 500 steps. What should you conclude?
Exercises
Work each problem before opening the solution.
- Exercise 1Core
Apply batch normalization to the batch for a single feature with , (ignore ). Then apply layer normalization to the single example treated as four features with , . Why do the numbers coincide, and when would they differ?
Show solution
Mean . Deviations , variance , standard deviation .
Normalized: .
Batch norm with , : .
Layer norm with , : .
The normalized values coincide only because the same four numbers were used once as a batch of one feature and once as one example with four features. In a real network the inputs are a matrix (batch by features): batch norm normalizes each column (a feature across examples), layer norm each row (an example across features). Batch norm's result for one example depends on the other examples in the batch; layer norm's does not.
- Exercise 2Core
Run two steps of AdamW by hand for a single parameter with , gradients and , , , , and weight decay . Show , , the bias-corrected values and after each step.
Show solution
Step 1. . . . , . Adam term . Decay term . .
Step 2. . . Bias corrections: , . . , . Adam term . Decay term . .
Notice that the gradient changed sign at step 2, but momentum kept the update positive (the parameter still decreased), and the first step moved by exactly in the Adam part, as the lesson predicted.
- Exercise 3Core
Explain why FP16 training needs loss scaling while BF16 usually does not. A gradient of arises in a layer: what happens to it in FP16 without scaling, and with a loss scale of ?
Show solution
FP16 has 5 exponent bits, giving a range from about (smallest subnormal; smallest normal is about ) up to 65,504. BF16 has 8 exponent bits, the same as FP32, so it represents values down to about (normal range) and gradients practically never underflow.
Without scaling, is below FP16's smallest subnormal and rounds to zero: that parameter receives no update, and if many gradients are this small, learning stalls silently.
With scale : , comfortably representable (though in the normal range it still has only about 3 significant digits). After backward, the scaler divides by 65,536 in FP32 before the optimizer step, recovering approximately . If scaling makes some gradient overflow above 65,504 (becoming inf), the dynamic scaler skips that step and halves the scale.
- Exercise 4Challenge
Write a PyTorch experiment that shows the effect of residual connections on gradient flow. Build a 30-layer network of width 64 with tanh activations and PyTorch's default initialization in two versions: (a) plain, ; (b) residual blocks . Do one backward pass on random data and print the gradient norm of the first and last linear layers' weights for each. Explain the expected result.
Show solution
import torch from torch import nn torch.manual_seed(0) D, L = 64, 30 class Plain(nn.Module): def __init__(self): super().__init__() self.layers = nn.ModuleList([nn.Linear(D, D) for _ in range(L)]) def forward(self, x): for lin in self.layers: x = torch.tanh(lin(x)) return x class Residual(nn.Module): def __init__(self): super().__init__() self.layers = nn.ModuleList([nn.Linear(D, D) for _ in range(L)]) def forward(self, x): for lin in self.layers: x = x + lin(torch.tanh(x)) return x x = torch.randn(128, D) for name, net in [("plain", Plain()), ("residual", Residual())]: net(x).pow(2).mean().backward() g_first = net.layers[0].weight.grad.norm().item() g_last = net.layers[-1].weight.grad.norm().item() print(f"{name:9s} first {g_first:.2e} last {g_last:.2e} ratio {g_first / g_last:.2e}")Expected: PyTorch's default
nn.Linearinitialization draws weights from , a variance of , well below what preserves gradient scale, and tanh's slope is at most 1. In the plain network the backward signal is multiplied by a factor below 1 at every layer, so the first layer's gradient norm is many orders of magnitude smaller than the last layer's: vanishing gradients. (A NumPy re-implementation with the same initialization and seed-dependent data gave a first/last ratio of about .) In the residual version the identity path gives , so the gradient reaching early layers always includes the upstream gradient carried unchanged along the skip path, and first- and last-layer gradient norms come out of the same order (ratio near 1 in the same re-implementation). Exact numbers depend on the seed; the many-orders-of-magnitude gap is the point. Adding pre-norm LayerNorm, as Transformers do, additionally keeps the residual stream's scale under control as depth grows.
Further reading
- paperGlorot & Bengio, Understanding the difficulty of training deep feedforward neural networks (AISTATS 2010)
- paperHe, Zhang, Ren & Sun, Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification (2015)
- paperIoffe & Szegedy, Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift (2015)
- paperHe, Zhang, Ren & Sun, Deep Residual Learning for Image Recognition (2015)
- paperLoshchilov & Hutter, Decoupled Weight Decay Regularization (2017; ICLR 2019)
- articleKarpathy, A Recipe for Training Neural Networks (2019)
Go further
Sources
Fact-checked and last reviewed 2026-11-05