Backpropagation Through Time
Imagine training a network to predict the next character in a sentence, one token at a time. The hidden state carries information from every previous character. A single output error at time step 100 might depend on a decision made at time step 3. How do you assign credit — or blame — across a hundred time steps of shared computation? This is the exact problem faced by every Recurrent Neural Network. Backpropagation Through Time (BPTT) is the algorithm that solves it. BPTT is not a new learning rule. It is ordinary backpropagation applied to a clever reformulation of the problem. A recurrent network reuses the same weight matrices at every time step. So BPTT "unrolls" the recurrence into an equivalent deep feedforward computational graph, one layer per time step. It then runs standard reverse-mode differentiation through it. The twist that makes RNNs special — and hard — is that the same weights appear at every layer of this unrolled graph. Their total gradient is therefore a sum of contributions from every time step. The chain rule forces repeated multiplication by the same recurrent weight matrix (or its Jacobian) over and over. That repeated multiplication is the root of one of deep learning's most consequential phenomena: the vanishing and exploding gradient problem. If the recurrent Jacobian systematically shrinks signals, gradients from distant time steps vanish before reaching early layers. The network then becomes effectively blind to long-range dependencies. If it amplifies signals instead, gradients explode into numerical instability. Understanding BPTT precisely matters a great deal. It is not just "backprop on an unrolled graph," but an algorithm with a specific complexity profile and a specific failure mode. This understanding is essential to grasping why plain RNNs struggle with long sequences. It also explains why architectures like LSTM and GRU were invented. This article gives a complete, mechanical account of BPTT. It covers the exact unrolling construction, and pseudocode for forward and backward passes. It also includes a worked numerical trace, and the complexity and correctness argument. Truncated variants used in practice are explained, too. Finally, it details the mathematical anatomy of vanishing and exploding gradients. By the end you should be able to derive the recurrent-weight gradient by hand. You should also be able to explain precisely why long sequences break plain RNNs.
- BPTT unrolls an RNN across time into a deep feedforward graph and applies standard backpropagation to it.
- The same weight matrices are shared at every time step, so their gradients are summed over all time steps.
- The chain rule through time forces repeated multiplication by the recurrent Jacobian, causing gradients to vanish or explode over long sequences.
- Time complexity is per sequence (linear in sequence length); memory is because every hidden state must be cached for the backward pass.
- Truncated BPTT trades exact long-range gradients for tractable memory and compute by limiting the backward window.
- The vanishing gradient problem motivates gated architectures — LSTM and GRU — that maintain more stable gradient flow.
BPTT is exact backpropagation on the time-unrolled RNN graph, where shared weights receive gradients summed across all time steps, and this repeated chain-rule multiplication through the recurrent Jacobian is precisely what makes gradients vanish or explode over long sequences.
The idea
A Recurrent Neural Network computes a hidden state from the current input and the previous hidden state , using the same parameters at every time step:
Because depends on , which depends on , and so on back to , the computation graph is not a simple chain of independent layers — it is a single recurrent cell applied times, with parameters tied across every application. Unrolling means writing out this graph explicitly for time steps as if it were a -layer feedforward network, where "layer " is the RNN cell instantiated with the actual and connected to layer 's output. Once unrolled, the graph is an ordinary computational graph with a clear topological order, and reverse-mode automatic differentiation (backpropagation) applies unchanged. The one subtlety — and it is the entire story of BPTT — is parameter tying. In a normal deep feedforward network, layer has its own weight matrix , and only involves that layer. In the unrolled RNN, every layer uses the identical . Backpropagation still computes a local gradient contribution at each time step, but because is one tensor, not separate tensors, its total gradient is the sum of the local contributions from every time step:
where the notation denotes "the gradient contribution computed using as it appears at time step 's copy in the unrolled graph." This sum-over-time is the defining structural feature of BPTT, distinguishing it from vanilla backpropagation in a non-recurrent network. The other defining feature is what happens to the hidden-state gradient (often called ) as it flows backward through time. Because , the chain rule gives
so propagating error from time step back to time step multiplies by this Jacobian times. Repeated multiplication by the same (or similar) matrix is exactly the setting in which linear algebra tells us eigenvalues dominate: if the spectral radius of is consistently below 1, the product shrinks geometrically (vanishing gradients); if consistently above 1, it grows geometrically (exploding gradients). This is developed rigorously in the Complexity & Correctness section.
How it works
BPTT proceeds in three conceptual phases: unroll, forward pass with caching, backward pass with gradient accumulation. 1. Unroll. Fix a sequence length (or a truncation window, see below). Instantiate the recurrent cell times, one per time step, all sharing the same parameters . 2. Forward pass. Run the RNN forward from (usually zero) to , computing and caching every intermediate quantity needed for the backward pass: each pre-activation , each hidden state , each output , and each per-step loss (e.g. cross-entropy against target ). The total loss is typically . 3. Backward pass. Walk time backward from down to . At each step, combine the gradient flowing in from the local output loss with the gradient flowing in from the future hidden state (i.e., from ), producing . Use to accumulate gradients into the shared parameter matrices, then propagate further backward into . The crucial bookkeeping variable is the carried hidden-state gradient, which plays the same role that "gradient flowing into a layer's input" plays in ordinary backprop, except here it also folds in the direct loss gradient at that time step. ```text PSEUDOCODE: Backpropagation Through Time (single sequence, length T) Inputs: x[1..T] # input sequence y[1..T] # target sequence W_xh, W_hh, W_hy # shared weight matrices b_h, b_y # shared biases h0 # initial hidden state (often zero vector)
# ---------- FORWARD PASS (cache everything needed for backward) ---------- h[0] = h0 for t = 1 to T: z[t] = W_xh @ x[t] + W_hh @ h[t-1] + b_h h[t] = phi(z[t]) # e.g. tanh o[t] = W_hy @ h[t] + b_y yhat[t] = psi(o[t]) # e.g. softmax L[t] = loss(yhat[t], y[t]) L_total = sum(L[t] for t in 1..T)
# ---------- BACKWARD PASS ----------
# Gradient accumulators for shared parameters, all initialized to zero dW_xh = 0; dW_hh = 0; dW_hy = 0 db_h = 0; db_y = 0 dh_next = 0 # gradient flowing back from step t+1 into h[t] for t = T downto 1: do_t = d(loss)/d(o[t]) # local output gradient, e.g. yhat[t]-y[t] dW_hy += do_t @ h[t].T db_y += do_t dh_t = W_hy.T @ do_t + dh_next # loss gradient + future-time gradient dz_t = dh_t * phi_prime(z[t]) # elementwise, backprop through phi dW_xh += dz_t @ x[t].T dW_hh += dz_t @ h[t-1].T db_h += dz_t dh_next = W_hh.T @ dz_t # this becomes "dh_next" for step t-1 return dW_xh, dW_hh, dW_hy, db_h, db_y ``` Two lines deserve emphasis because they encode the two structural facts from the previous section. The line `dW_hh += dz_t @ h[t-1].T` (rather than `=`) implements gradient summation over shared parameters. The line `dh_t = W_hy.T @ do_t + dh_next` implements the merging of local loss gradient and carried-forward-in-time gradient — this addition is exactly the multivariate chain rule applied to a node () that has two downstream consumers: the output and the next hidden state . After computing gradients, an ordinary optimizer step (SGD, Adam, etc.) updates using the accumulated gradients, exactly as in feedforward backprop.
Truncated BPTT. For long or streaming sequences, caching all hidden states and backpropagating through all of them is often infeasible. Truncated BPTT (TBPTT) runs the forward pass continuously but only backpropagates through the last time steps (a fixed window), detaching (stopping gradient flow at) the boundary. This trades exactness — gradients no longer reflect dependencies older than steps — for bounded memory and bounded per-step compute, which is essential for training on very long or infinite streams. ```text PSEUDOCODE: Truncated BPTT with window k, chunk size k (common variant: k1 = k2 = k) h = h0 for chunk_start = 1, 1+k, 1+2k, ... up to T: chunk = x[chunk_start .. chunk_start+k-1] h = h.detach() # cut gradient flow into earlier chunks run forward pass over chunk, starting from h, caching intermediates compute chunk loss backward pass over just this chunk (as in full BPTT pseudocode above) accumulate/apply parameter gradients h = final hidden state of this chunk # carried forward, but gradient-detached ```
Complexity & correctness
Time complexity. The forward pass performs matrix–vector work per time step (dominated by , , and , each costing or for hidden size ), so the forward pass over a length- sequence is . The backward pass performs the same amount of matrix work per time step (each line in the backward loop is one matrix–vector product), so it is also . Total: time, i.e. linear in sequence length, same asymptotic order as the forward pass — this is the general fact that reverse-mode autodiff costs a small constant factor (roughly 2–3×) times the forward pass, regardless of graph shape.
Space complexity. The forward pass must cache every (and whatever the elementwise nonlinearity needs to compute its local derivative) because the backward pass visits time steps in reverse order and needs these values. This gives memory for full BPTT — memory that grows linearly with sequence length, which is the practical reason long sequences are expensive to train on and the reason truncated BPTT exists (it caps memory at regardless of total stream length).
Correctness argument. BPTT's correctness reduces entirely to two facts, both standard in reverse-mode differentiation: 1. Unrolling is a faithful reformulation. The unrolled graph, with copies of the cell connected in a chain and tied weights, computes exactly the same function of as the original recurrence — it is the same computation written out explicitly rather than expressed with a self-reference. Therefore any correct differentiation procedure on the unrolled graph yields the correct gradient of the original recurrent function. 2. The multivariate chain rule handles shared parameters and fan-out correctly. When a single scalar (or tensor) parameter feeds into multiple downstream computations — here, feeding into , and feeding into both and — the total derivative is the sum of the derivatives along each path, by the multivariable chain rule:
The pseudocode's `+=` accumulation and the `dh_t = ... + dh_next` line are literal implementations of these two identities. Since reverse-mode autodiff is known to compute exact gradients on any directed acyclic computational graph (a standard result inherited from ordinary backpropagation), and the unrolled RNN graph is exactly such a DAG, BPTT computes the exact gradient of the sequence loss with respect to every shared parameter.
Why gradients vanish or explode. Unrolling the recursive Jacobian relation gives, for the gradient flowing from time back to time ,
This is a product of matrices. If were a scalar and were bounded by a constant , this product behaves like : for it shrinks exponentially in (vanishing gradient — early time steps get essentially zero learning signal from distant losses), and for it grows exponentially (exploding gradient — parameter updates become enormous and unstable, or overflow to NaN). For matrices, the rigorous statement uses the spectral radius of evaluated along the trajectory: if consistently, the largest singular values of the product decay geometrically in ; if , they grow geometrically. With or sigmoid activations, (and is often much less than 1 away from the origin), which biases the product toward vanishing — this is why plain ("vanilla") RNNs empirically struggle to learn dependencies spanning more than a few dozen time steps, a phenomenon documented well before gated architectures existed.
Tradeoffs, honestly stated.
- Full BPTT is exact but in both time and memory — infeasible for very long sequences (e.g. streaming audio, book-length text) without truncation.
- Truncated BPTT bounds memory and compute but introduces bias: gradients cannot represent dependencies longer than the truncation window, so the network is structurally prevented from learning them, no matter how much data it sees.
- Gradient clipping (rescaling the gradient norm if it exceeds a threshold) is a cheap, effective mitigation for exploding gradients, but does nothing for vanishing gradients — it only bounds the magnitude of instability, not the loss of long-range signal. This asymmetry is precisely why architectural fixes were needed.
Worked example
Consider a toy scalar RNN (hidden size 1, so all "matrices" are scalars) with , no biases, mapped through a linear readout , trained with squared-error loss , over steps. Let
Inputs , targets .
Forward pass.
| 1 | ||||
| 2 | ||||
| 3 |
Backward pass. Use : , , . Start with and step :
- t = 3: . .
. . ; . (passed to ).
- t = 2: . → running total .
. . → running total ; → running total . (passed to ).
- t = 1: . → running total .
. . → final ; → final .
Final accumulated gradients: , , . Notice concretely how the recurrent-weight gradient is a sum of three terms (), one per time step, exactly as the theory predicted — and notice how carries information from back to , shrinking somewhat at each hop because and getting rescaled by ; with a smaller or deeper unrolling this shrinkage compounds into the vanishing-gradient regime described above.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Overwriting instead of accumulating gradients for shared weights | Treating like a per-layer parameter (`=` instead of `+=`) | Always accumulate (`+=`) gradients for every tied parameter across all time steps |
| Forgetting to add to the local output gradient | Missing the "fan-out" chain rule term from feeding both and | Explicitly sum both contributions before backpropagating through |
| Not detaching hidden state between TBPTT chunks | Believing gradient must "keep flowing" for correctness | Detach at chunk boundaries by design — that is the whole point of truncation |
| Caching too little in the forward pass | Discarding or to save memory, then needing them in backward pass | Cache every quantity the backward pass reads, or recompute via gradient checkpointing |
| Blaming exploding gradients solely on learning rate | Instability actually comes from repeated multiplication by with spectral radius | Apply gradient clipping and/or inspect 's spectral radius, not just the learning rate |
| Assuming truncated BPTT is "basically exact" | Believing a large enough window recovers full gradients | Recognize TBPTT introduces a structural bias — dependencies beyond the window are provably invisible to gradients |
Pioneers & history
The unrolling idea and the general reverse-mode differentiation machinery behind BPTT trace back to the broader development of backpropagation for feedforward networks, but its specific application to recurrent networks — treating time as depth — was articulated explicitly in the mid-1980s and formalized soon after. Paul Werbos described the "backpropagation through time" formulation in the context of dynamical systems, and the technique was subsequently popularized within the connectionist community alongside the broader adoption of backpropagation itself.
Origins · 1 minPaul J. Werbos and how this was discovered
Werbos developed the mathematical framework for propagating error gradients through time-unrolled dynamical systems, extending the chain-rule-based gradient computation that underlies all of modern backpropagation into settings with temporal recurrence — the exact operation this article calls BPTT.
Where this matters
BPTT is the training engine behind every sequence model built on recurrence, and its failure modes directly motivated the architectures that dominate sequence modeling today. Language models built on LSTM and GRU cells — used historically in machine translation, speech recognition, and text generation before the rise of attention-based models — are trained with BPTT (or its truncated variant) at their core; the gating mechanisms in those architectures exist specifically to create more stable gradient paths through time, directly addressing the vanishing-gradient analysis derived above. Speech recognition systems that process audio streams frame-by-frame use truncated BPTT to keep training tractable over very long recordings. Time-series forecasting systems in finance and demand prediction, sequence tagging in NLP pipelines, and any online/streaming RNN deployment (e.g. on-device keyboard prediction) rely on the exact mechanics — accumulation over shared weights, caching for the backward pass, and truncation windows — described here. Even in the transformer era, understanding BPTT remains foundational: it is the conceptual bridge that explains why attention mechanisms and gated recurrent units were considered necessary advances, and it remains the training algorithm for any modern architecture that retains explicit recurrent state (e.g. certain streaming or state-space sequence models).
Ask the tutor: How exactly do LSTM gates modify the recurrent Jacobian to prevent gradients from vanishing the way they do in a vanilla RNN?
Is BPTT a different algorithm from backpropagation?
No — BPTT is standard backpropagation applied to the unrolled computational graph of a recurrent network. There is no separate mathematical machinery; reverse-mode automatic differentiation on a DAG works identically whether that DAG came from a feedforward network or from unrolling a recurrence over time. The only genuinely new bookkeeping is that BPTT must (1) sum gradient contributions for parameters that are shared/tied across every unrolled time step, and (2) merge, at each hidden state, the gradient arriving from that time step's own output with the gradient arriving from the next time step. Both are direct, unremarkable applications of the multivariate chain rule, not new theory.
Why do gradients vanish specifically in RNNs but not (as severely) in typical feedforward networks?
Feedforward networks generally have independent weights per layer, so there's no forced repeated multiplication by the same matrix. In an RNN, backpropagating error from time step to time step requires multiplying by the same Jacobian roughly times — a genuine matrix power. Very deep feedforward networks can suffer an analogous problem (which is one reason residual connections were introduced), but plain RNNs applied to long sequences hit this issue especially severely because sequence lengths (hundreds or thousands of steps) routinely exceed typical feedforward network depths (tens to low hundreds of layers).
What exactly does truncated BPTT sacrifice compared to full BPTT?
Full BPTT computes the exact gradient of the total sequence loss with respect to every parameter, using dependencies across the entire sequence. Truncated BPTT only backpropagates through the last time steps of the current chunk, then detaches — so any dependency an output at time has on inputs more than steps earlier receives literally zero gradient signal, not just a small one. This is a structural, not statistical, limitation: no amount of additional training data changes it. In exchange, TBPTT bounds memory and compute to regardless of total stream length, which is what makes training on very long or effectively infinite sequences (like continuous audio or logging streams) practical at all.
How does BPTT relate to the vanishing gradient problem in a precise mathematical sense?
The hidden-state-to-hidden-state gradient equals a product of Jacobians, each of the form . Products of many matrices are governed by the spectral radius of the matrices involved: values below 1 cause exponential shrinkage in the product's norm as the number of factors grows, and values above 1 cause exponential growth. Since and sigmoid derivatives are bounded above by 1 (and often much smaller away from zero), the product is biased toward shrinking, which is the formal statement of "vanishing gradients." This is a property of the architecture and activation choice, not of any particular training run going wrong.
Does BPTT still matter now that attention-based models are common?
Yes, for two reasons. First, many production and research systems still use explicit recurrent state — streaming models, certain state-space sequence models, and on-device systems where constant-memory-per-step inference matters — and these are trained with BPTT or truncated BPTT. Second, and more importantly for learning the field, BPTT's failure analysis (vanishing/exploding gradients through repeated Jacobian multiplication) is the historical and conceptual reason gated units and, later, attention mechanisms were developed. Understanding BPTT is what makes the design motivations behind LSTM, GRU, and even residual/attention-based alternatives legible rather than arbitrary.
Why must the forward pass cache every hidden state instead of recomputing them during the backward pass?
Technically, recomputation is possible — this is exactly the idea behind gradient checkpointing, which trades compute for memory. But without checkpointing, the backward pass needs , , and other forward quantities at every time step because the local Jacobians (, and the outer products used to accumulate , ) are functions of those specific values. If they aren't cached, they would need to be recomputed by rerunning the forward pass from scratch up to each time step, which is far more expensive than simply storing them, hence the standard memory cost of full BPTT.
What's a common way to detect that vanishing gradients are hurting training, in practice?
A useful diagnostic is a synthetic long-range dependency task — for example, requiring the network to copy or recall a signal presented many time steps earlier — since ordinary loss curves on real data can plateau in ways indistinguishable from garden-variety optimization difficulty. Another diagnostic is directly inspecting the norm of as a function of across a long sequence: if it decays sharply for small (far in the past), long-range gradients are vanishing. Comparing performance against a gated architecture (LSTM or GRU) trained identically is also a strong practical signal, since those architectures are specifically designed to mitigate this failure mode.