Backpropagation Through Time › Gradient Flow Through Hidden States
Language AI HandbookAll chapters Backpropagation Through Time › Gradient Flow Through Hidden StatesOne section · Sections in this chapter9 / 15 ↓
Reading tools Definitions & inline links
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
The quality of gradient signal reaching early timesteps determines whether the model can learn long-range dependencies. We can track how gradient norms decay as we move further back in time using PyTorch's retain_grad():
def gradient_decay_by_position(
seq_len, vocab_size=27, hidden_size=64, batch_size=4
):
"""
Compute gradient norms as a function of distance from the end of the sequence.
Uses manual RNN forward pass to retain gradients at each hidden state.
"""
torch.manual_seed(0)
m = SimpleRNN(vocab_size, hidden_size, vocab_size)
x, targets = make_batch(batch_size, seq_len, vocab_size)
# Initial hidden state: requires_grad=True so retain_grad() works
h_prev = torch.zeros(batch_size, hidden_size, requires_grad=True)
all_h = []
# Manual forward to retain all hidden state grads
for t in range(seq_len):
x_t = x[:, t, :] # (batch, input_size)
h_new = torch.tanh(
x_t @ m.rnn.weight_ih_l0.T
+ m.rnn.bias_ih_l0
+ h_prev @ m.rnn.weight_hh_l0.T
+ m.rnn.bias_hh_l0
) # (batch, hidden_size) - requires_grad=True because it depends on parameters
h_new.retain_grad()
all_h.append(h_new)
h_prev = h_new
# Apply the loss only at the final timestep. This isolates the gradient
# propagated backward through the recurrent chain instead of adding a
# direct loss contribution at every hidden state.
logits = m.fc(all_h[-1])
loss = criterion(logits, targets[:, -1])
loss.backward()
grad_norms = []
for h_t in all_h:
if h_t.grad is not None:
grad_norms.append(h_t.grad.norm().item())
else:
grad_norms.append(0.0)
return grad_norms
seq_len_decay = 30
grad_decay = gradient_decay_by_position(seq_len_decay)
The gradient decay plot reveals why vanilla RNNs struggle with long-range dependencies. Early timesteps receive only a tiny fraction of the gradient signal that the later timesteps receive. This asymmetry means the model updates its weights based primarily on recent history, making it difficult to learn patterns that require looking far back in time.
This plot also reveals something subtle about the nature of learning in RNNs: even if a model could in principle encode long-range dependencies in its hidden state (and a sufficiently large hidden state can represent very complex functions), training with BPTT cannot teach it to do so if the gradients decay before reaching the relevant early timesteps. The representational capacity of the model and the trainability of that capacity are two different things. A vanilla RNN might theoretically be able to remember that a sentence began with "The fluffy orange", but BPTT cannot reliably teach it to do so if those words appeared many steps ago.
The LSTM and GRU architectures, introduced in the following chapters, address this limitation through gating mechanisms that provide more direct gradient paths through time. By using learned gates to selectively write to and read from a cell state, LSTMs can maintain gradients that are closer to 1.0 in magnitude over long distances, allowing gradient signal to travel dozens or hundreds of steps without severe attenuation.