Weight initialization
Start weights at the right scale.
Create a free account Try part of it here
Why it matters
Before training starts, every weight in a network needs a first value. That choice is called initialization. Pick values that are too big and the numbers flowing through a deep network blow up within a few layers. Pick values that are too small and they fade to nothing. Either way, training stalls before it begins. Good starting values were one of the quiet fixes that made deep networks trainable. You'll still find an initialization line near the top of every large language model's code (an LLM is the kind of model behind chat assistants).
What you'll be able to do
- Explain why all-zero starting weights can't work (symmetry)
- Show how badly scaled weights make signals explode or vanish through many layers
- Choose Xavier/Glorot or He/Kaiming initialization from the activation function
- Recognize the initialization choices built into PyTorch layers and LLMs
What it covers
- Why not start at zero?
- The size of the weights matters too
- How big should the weights be?
- Worked example: a 50-input ReLU layer
- The two standard recipes
- In PyTorch and in LLMs
Why not start at zero?
Picture a team of four new hires. They get the same notes on day one. After each task, they get the same feedback. Nothing ever makes them different, so they keep doing identical work. You hired four people and got one.
A layer of neurons behaves the same way. Suppose every weight in a layer starts at the same value, say zero or 0.1. Then every neuron in that layer computes the same output. During training, each one also gets the same nudge (the same gradient). So they update in the same way and stay identical forever. The whole layer acts like a single neuron.
Random starting values fix this. Each neuron starts a little different, so each one learns something different. This is called breaking the symmetry.
The code below feeds two examples into a layer of 4 hidden neurons. It prints the outputs for the first example. It tries three starting choices: all zeros, all 0.1, and random values.
import numpy as np X = np.array([[0.5, -1.0, 2.0], [1.5, 0.3, -0.7]]) for name, W1 in [("all zeros", np.zeros((3, 4))), ("all 0.1", np.full((3, 4), 0.1)), ("random", np.random.default_rng(0).normal(0, 0.5, (3, 4)))]: H = np.tanh(X @ W1) # 4 hidden neurons print(f"{name:<10} hidden outputs for example 1: {np.round(H[0], 3)}")
With all zeros, every neuron outputs 0. With all 0.1, every neuron gets the same weighted sum: 0.1 × (0.5 − 1.0 + 2.0) = 0.15. Then tanh turns that into about 0.149, four times over. Only the random start gives four different answers. With equal weights, the four neurons say the same thing now, and they always will.
Show the answer
Still identical to each other
They start equal, so they get equal gradients and equal updates at every step. Nothing ever breaks the tie.
The size of the weights matters too
Random is not enough. The random values also need the right size.
Think of a photocopier set to 110%. Copy a picture, then copy the copy, and keep going. After 50 rounds, the picture is over 100 times as big. Set it to 90% and it shrinks to about 1/200 of its size instead. Only 100% keeps it the same size.
Each layer of a network is like one round of copying. A neuron adds up many inputs, each times a weight. If the weights are too big, those sums grow a bit at every layer. If they're too small, the sums shrink a bit at every layer. Over many layers, a little growth or shrinkage adds up fast.
The code below pushes a batch of 500 examples through 20 ReLU layers. Each layer is 256 neurons wide. It tries three weight sizes. The size is set by the standard deviation of the random weights: the typical distance of a weight from zero. After each layer, the code measures the spread (standard deviation) of the outputs.
import numpy as np rng = np.random.default_rng(0) x = rng.normal(size=(500, 256)) width = 256 for label, std in [("std 1.0", 1.0), ("std 0.01", 0.01), ("He/Kaiming: sqrt(2 / 256)", np.sqrt(2 / width))]: h, spreads = x, [] for layer in range(20): h = np.maximum(0, h @ rng.normal(0, std, (width, width))) spreads.append(h.std()) print(f"{label:<27} layer 1: {spreads[0]:.3g} layer 10: {spreads[9]:.3g} layer 20: {spreads[19]:.3g}")
With standard deviation 1, the outputs grow about elevenfold at every layer. By layer 20 they're astronomically large. With 0.01, they shrink toward zero. When the outputs fade like that, the gradients fade with them, and the early layers stop learning. The third setting keeps the spread in the same ballpark, between about 0.5 and 1, all the way through. The next section shows where its number comes from.
Good starting weights keep each layer's scaling near 1, so the signal survives.
How big should the weights be?
You met variance in Phase 2. It measures spread: how far values sit from their average. The standard deviation is its square root. A standard deviation of 0.1 means a variance of 0.01.
A layer's output spread depends on three things:
- , the number of inputs each neuron adds up. This is called the fan-in.
- The variance of the weights.
- The variance of the inputs.
In plain words: more inputs means more terms in the sum, so a bigger total. Bigger weights also mean a bigger total. Multiply the three together to get a rough idea of the output's spread.
This rule is a rough guide, not an exact law. It assumes the weights and inputs are random and centered on zero.
Now try it with the numbers from the code above. There are 256 inputs. The weights have standard deviation 1, so variance 1. The inputs have variance 1. The output variance is about 256 × 1 × 1 = 256.
ReLU then turns every negative number into 0. That throws away about half the signal, so roughly 128 is left. Take the square root to turn variance back into spread: √128 ≈ 11.3. So every layer makes the spread about 11 times bigger. That's the elevenfold growth you saw.
To keep the spread steady, you want each layer's multiplier to be 1. That means choosing the weight variance so that × weight variance ÷ 2 = 1. The answer is a weight variance of 2 ÷ 256, or a standard deviation of √(2/256). That's the third setting in the code.
import math n = 256 std = 1.0 print(round(math.sqrt(n * std**2 / 2), 1))
Show the answer
11.3
256 × 1 = 256, half of that is 128, and √128 ≈ 11.3. That's the elevenfold growth per layer.
Worked example: a 50-input ReLU layer
Each neuron in a ReLU layer adds up 50 inputs. Compare two weight sizes: standard deviation 0.5, and standard deviation 0.2. How much does each one multiply the spread per layer, and after 10 layers?
import math n_in = 50 for std in [0.5, 0.2]: growth = math.sqrt(n_in * std**2 / 2) # spread multiplier per ReLU layer print(f"std {std}: x{growth:.2f} per layer, x{growth**10:,.0f} after 10 layers")
Start with standard deviation 0.5. Its variance is 0.5 × 0.5 = 0.25. Multiply by the 50 inputs: 50 × 0.25 = 12.5. ReLU keeps about half, which leaves 6.25. The square root is 2.5. So each layer makes the spread 2.5 times bigger. After 10 layers that's 2.5 multiplied by itself 10 times, about 9,537.
Now standard deviation 0.2. Its variance is 0.04. Then 50 × 0.04 = 2, and half of that is 1. The square root of 1 is 1, so the spread stays the same at every layer. Note that 0.2 is √(2/50). It's the same recipe as before: 2 divided by the fan-in, then a square root.
The two standard recipes
That recipe has a name. Two recipes cover most layers. Each is named after the researchers who proposed it. Both use the fan-in, . One also uses the fan-out, : the number of outputs the layer produces.
| Name | Weight standard deviation | Use with |
|---|---|---|
| Xavier / Glorot | tanh, sigmoid, linear layers | |
| He / Kaiming | ReLU and its relatives |
In plain words: for a ReLU layer, divide 2 by the number of inputs and take the square root. For a tanh or sigmoid layer, divide 2 by the inputs plus the outputs, then take the square root. Wider layers get smaller weights.
He initialization has the extra factor of 2 because ReLU zeroes about half its inputs. Doubling the weight variance makes up for the half that's lost.
Here's a small example. A layer has 256 inputs and 128 outputs. He gives √(2/256) ≈ 0.0884. Xavier gives √(2/384) ≈ 0.0722. In Python:
import math n_in, n_out = 256, 128 print("He: ", round(math.sqrt(2 / n_in), 4)) print("Xavier:", round(math.sqrt(2 / (n_in + n_out)), 4))
Go deeper Why Xavier uses both the inputs and the outputs
Signals travel two ways through a network. On the forward pass, each neuron adds up inputs. Keeping that steady wants a weight variance of . During training, gradients flow backward. Each input then collects gradients from neurons. Keeping that steady wants .
You can't have both unless the two numbers are equal. Xavier Glorot and Yoshua Bengio split the difference. They used the average of the two counts, which gives a variance of . The 2 here comes from that averaging. There's no extra ReLU correction, because tanh and sigmoid don't throw half their inputs away.
Show the answer
√(2/400) ≈ 0.071
He initialization is √(2 / n_in). With 400 inputs that's √(2/400) ≈ 0.071.
In PyTorch and in LLMs
You rarely write these formulas yourself. PyTorch layers start with sensible values already. You can switch to a different recipe in one line:
import torch.nn as nn layer = nn.Linear(256, 128) # already initialized: uniform in ±1/√256 for weights and bias nn.init.kaiming_normal_(layer.weight, nonlinearity="relu") # switch to He initialization nn.init.zeros_(layer.bias)
Starting the biases at zero is fine. The random weights already make the neurons differ.
Large language models (LLMs) make their own choices. GPT-2, which is a 2019 language model from OpenAI, draws its weights from a normal distribution with standard deviation 0.02. A few of its layers add their results onto a running total that flows through the whole model. Those layers get scaled down further, by . Here "layers" means the number of blocks the model stacks.
In plain words: the more blocks a model has, the more things get added to that running total. So each one starts smaller, and the total doesn't grow out of control.
A small example: a 12-layer model gives 1/√24 ≈ 0.204. So those layers start with standard deviation 0.02 × 0.204 ≈ 0.0041.
import math layers = 12 scale = 1 / math.sqrt(2 * layers) print(round(scale, 3), round(0.02 * scale, 4))
Go deeper Why "2 ×" the number of layers
The running total is called the residual stream. Each block of GPT-2 adds to it twice: once from its attention part, which is the part that mixes information between tokens, and once from its MLP part. So a 12-block model adds 24 things to the stream. Each addition makes the spread a little bigger. Scaling each one by makes all 24 additions together grow the stream about as much as a single unscaled one would. You'll meet these parts properly in the transformer lessons.
Later lessons cover residual connections and normalization layers, which are two more ways to keep signals in a healthy range. Together with good initialization, they're why networks with 100 layers can train at all.
Symptoms of bad initialization
- The loss is
nanor huge from the first step. The weights are probably too large. (It can also be a learning rate problem.) - The loss barely moves, and the gradients in the early layers are tiny. The weights may be too small. Or sigmoid and tanh units may be stuck at their flat ends, called saturated.
- Several neurons in a layer always have identical values. They started identical.
Show the answer
The starting weights are too large, so the numbers explode through the layers
Weights that are too big multiply the spread at every layer. After 30 layers the numbers are enormous or overflow, so the loss is huge or nan. Too-small weights make the loss barely move instead.
Practice what you just read
- 6 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 4 review cards for spaced repetition
Try a quiz question
What goes wrong if every weight in a hidden layer starts at the same value?