Backpropagation Through Time › Gradient Flow Through Hidden States

Michael BrenndoerferMay 10, 20254 min read
Language AI HandbookAll chapters
Part 11 of 67Recurrent Neural Networks
Chapter 75Backpropagation Through Time › Gradient Flow Through Hidden States
Reading section 9 of 15 · Gradient Flow Through Hidden States · 4 minSection 9 of 15 · 4 min

Book contents

Section 9 of 15 · 4 min read

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():

In[16]:
Code
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)
Out[17]:
Visualization
Log-scale line plot showing gradient norms increasing toward the final timestep of a 30-step sequence, with the earliest states receiving the weakest signal.
Gradient norm at each timestep's hidden state for a vanilla RNN processing a 30-step sequence. The gradient norm decreases sharply as we move from the final timestep back toward the beginning of the sequence, showing how error signals attenuate when backpropagated through many matrix multiplications.

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.