Quant Memo
Core

Backpropagation Through Time

A recurrent network reuses the same weights at every timestep, so training it means adding up how much each timestep's error depends on those shared weights, chained all the way back through the sequence. That chaining is backpropagation through time, and it is also where recurrent networks quietly fail.

Prerequisites: Backpropagation, Recurrent Neural Networks

A recurrent network processes a sequence one step at a time, carrying a hidden state forward and using the same set of weights at every single step — that weight sharing is what lets it handle sequences of any length with a fixed number of parameters. But it creates a puzzle for training: if a mistake shows up at timestep 50, and the weights that produced it were also used at timesteps 1 through 49, how much of the blame belongs to each use? Ordinary backpropagation was built for a single forward pass through a fixed stack of distinct layers. A recurrent network run for 50 steps behaves, for gradient purposes, like a 50-layer network where every layer happens to share identical weights — and the gradient has to account for that sharing correctly, not just once, but by summing the weight's influence across every single timestep it touched.

The analogy: one employee blamed across fifty shifts

Imagine one employee works the same register, using the exact same habits, for fifty shifts in a row, and at the end of the month the store finds a total cash shortfall. To figure out how much to adjust that employee's training, you can't just look at shift 50 — the habits that caused shift 50's shortfall are the same habits used in shifts 1 through 49, so you need to trace the shortfall's cause back through every shift those habits touched and add up the total damage attributable to that one set of habits. That's backpropagation through time: unroll the recurrent network into "one copy per timestep" that all secretly share the same weights, run ordinary backpropagation on the unrolled chain, and then sum the gradient contributions from every timestep back onto the single shared weight, because that one weight was responsible for the error at every step it participated in.

The maths

A vanilla recurrent network updates its hidden state hth_t from the previous state and the current input:

ht=tanh(Whht1+Wxxt+b)h_t = \tanh(W_h h_{t-1} + W_x x_t + b)

In words: the new hidden state blends the old hidden state ht1h_{t-1} (via weight matrix WhW_h) with the new input xtx_t (via WxW_x), squashed through tanh\tanh. Crucially, WhW_h, WxW_x, and bb are the same matrices and vector at every timestep tt — there is only one copy of each, reused TT times for a length-TT sequence.

To train, you need L/Wh\partial \mathcal{L}/\partial W_h, where L\mathcal{L} is the total loss summed over all timesteps. Because WhW_h was used at every step, the chain rule requires summing its influence across all of them:

LWh=t=1TLtWh\frac{\partial \mathcal{L}}{\partial W_h} = \sum_{t=1}^{T} \frac{\partial \mathcal{L}_t}{\partial W_h}

In words: work out how much timestep tt's loss Lt\mathcal{L}_t would change if you nudged WhW_h, do that separately for every timestep, then add all TT contributions together — because WhW_h is literally one shared variable, its total gradient is the sum of every place it was used.

Each individual term Lt/Wh\partial \mathcal{L}_t / \partial W_h itself requires chaining back through every hidden state between WhW_h's use at some earlier step and the loss at step tt:

LtWh=k=1tLtht(j=k+1thjhj1)hkWh\frac{\partial \mathcal{L}_t}{\partial W_h} = \sum_{k=1}^{t} \frac{\partial \mathcal{L}_t}{\partial h_t} \left( \prod_{j=k+1}^{t} \frac{\partial h_j}{\partial h_{j-1}} \right) \frac{\partial h_k}{\partial W_h}

In words: to find how the loss at step tt depends on the weight's use at some earlier step kk, you multiply together the local derivative hj/hj1\partial h_j / \partial h_{j-1} at every single step in between — a long product of Jacobian matrices, one per timestep spanned. This product is the crux of the whole method, and also its Achilles' heel: each factor is typically well below 1 in magnitude (because tanh\tanh's derivative maxes out at 1 and is usually smaller), so a long product of them shrinks toward zero as tkt - k grows. That is the vanishing gradient problem, and it means the network effectively stops learning from anything that happened more than a handful of steps in the past.

h1 h2 h3 h4 shared weight Wh reused at every arrow gradient from loss at t=4 signal has nearly vanished
Forward, one shared weight is reused at every step. Backward, the gradient from a late timestep's loss must chain through every step in between, shrinking (thinner dashed line) the further back it travels.

Worked example 1: the vanishing product, with real numbers

Suppose the local Jacobian hj/hj1\partial h_j/\partial h_{j-1} has magnitude roughly 0.60.6 at every step — plausible once tanh\tanh has saturated somewhat and WhW_h isn't unusually large. To find how much the loss at t=10t=10 depends on the hidden state at k=2k=2, the chain requires the product across 8 intervening steps:

j=310hjhj10.68=0.0168\prod_{j=3}^{10} \frac{\partial h_j}{\partial h_{j-1}} \approx 0.6^{8} = 0.0168

Only 1.7% of the gradient signal from step 10's loss survives the trip back to step 2. Push the gap to 20 steps instead of 8:

0.6200.00003660.6^{20} \approx 0.0000366

That's a signal reduced to 0.0037% of its original size — for all practical purposes zero. Any dependency the network needed to learn that spans 20 or more timesteps is invisible to gradient descent, not because the pattern isn't there in the data, but because the training signal that would teach it has decayed to numerical noise before it arrives.

Worked example 2: truncated BPTT and its cost

Training on a 10,000-step sequence by unrolling the full thing is both slow and where the vanishing product is at its worst. Truncated BPTT instead breaks the sequence into chunks — say chunks of 20 — and only backpropagates within each chunk, carrying the hidden state forward across chunk boundaries but not the gradient.

Consider a dependency in the data between an event at absolute position 1,005 and an outcome at position 1,015 (10 steps apart, within one chunk boundary): truncated BPTT with chunk length 20 can, in principle, still learn it, since both fall in the same or adjacent windows.

But a dependency between position 1,000 and position 1,050 (50 steps apart) spans more than two chunk boundaries. Even though the hidden state carries some trace of position 1,000 forward mechanically, no gradient ever flows back across the chunk boundary to explicitly teach the network "the outcome at 1,050 depends on what happened at 1,000" — that credit assignment is structurally impossible with chunk length 20 spanning only local windows. Truncation trades the ability to learn long dependencies for tractable compute; the trade-off is explicit and the chunk length is the parameter that sets how much you're giving up.

Gradient descent
parameter →
position -0.623loss -0.141gradient -0.930converging

Watch how a step that's too small crawls without visibly progressing — that's the practical symptom of vanishing gradients in a recurrent network: the update to WhW_h from a distant timestep is so small it's nearly indistinguishable from no update at all, even though the optimizer is technically still running.

Backpropagation through time is ordinary backpropagation applied to a recurrent network unrolled into a chain of timesteps that all share one set of weights — so the shared weight's gradient is a sum across timesteps, and each term in that sum requires a product of per-step Jacobians that shrinks (or, less often, explodes) geometrically with the distance spanned. This product is the direct cause of why plain RNNs struggle with long-range dependencies.

What this means in practice

Any time a plain RNN seems to "forget" something from many steps back — an order-book imbalance from an hour ago that should still matter, a macro release from earlier in the session — the vanishing gradient through BPTT is very often the actual mechanism, not a data problem. This is precisely the failure mode that motivated gated architectures like the LSTM, which add a separate additive pathway for the cell state specifically so its local Jacobian along that path stays close to 1 rather than shrinking every step. When training any recurrent architecture, gradient clipping (capping the gradient's norm) guards against the opposite failure — exploding gradients, where the same product grows rather than shrinks — and is close to mandatory in practice.

The trap: assuming more unrolled timesteps always means "the network sees more context" in a useful way. Beyond the distance where the Jacobian product has decayed to near-zero — often just 10 to 20 steps for a vanilla RNN — additional unrolled history contributes essentially nothing to training, only extra compute and memory. Diagnosing "my RNN isn't using long-range information" by adding more layers or more hidden units doesn't fix this; it's a gradient-flow problem, not a capacity problem, and the fix is architectural (gates, skip paths, or attention), not "make the same mechanism bigger."

Related concepts

Practice in interviews

Further reading

  • Werbos, Backpropagation Through Time: What It Does and How to Do It (1990)
  • Goodfellow, Bengio & Courville, Deep Learning, ch. 10
ShareTwitterLinkedIn