RNNs and LSTMs — Sequence Modelling
Hidden states, vanishing gradients across time, and how LSTMs use gates to selectively remember and forget. Built from scratch before PyTorch.
A CNN sees one image independently. An MLP sees one row independently. But a sentence, a stock price, a user session — each step depends on what came before. RNNs process sequences by carrying memory forward.
Amazon wants to predict whether a user will make a purchase in the next 10 minutes based on their browsing session: home page → search "running shoes" → product page → add to cart → remove from cart. An MLP treats each action independently — it sees five inputs with no concept of order or context. The sequence matters enormously. "Add to cart then remove" signals hesitation. "Search then product page" signals intent. The temporal pattern is the signal.
RNNs (Recurrent Neural Networks) process sequences one step at a time, maintaining a hidden state — a vector that summarises everything seen so far. At each step the hidden state is updated using the current input and the previous hidden state. After processing the full sequence, the final hidden state is a compressed representation of the entire sequence — used for classification, regression, or generation.
Reading this sentence word by word — your understanding of each new word depends on everything you have read before. "The bank was steep" versus "The bank was closed" — the word "bank" means something different based on prior context. You carry a mental model forward as you read. That mental model is the hidden state.
An RNN does exactly this — it maintains a hidden state vector that gets updated at every word (or time step). The hidden state at the end of the sequence encodes the full context. The problem: RNNs forget things from 20+ steps ago. LSTMs fix this with explicit memory management using gates.
The RNN cell — one equation, applied at every time step
An RNN cell has one equation. At each time step t it takes the current input xₜ and the previous hidden state hₜ₋₁, combines them linearly, and applies tanh to produce the new hidden state hₜ. The same weights Wₓ, Wₕ, and bias b are reused at every time step — weight sharing across time, just as CNNs share weights across space.
LSTM — three gates that control what to remember, forget, and output
The LSTM (Long Short-Term Memory) was designed specifically to fix the vanishing gradient problem. It maintains two states: the hidden state hₜ (same as RNN) and a new cell state Cₜ — a separate memory lane that runs through the sequence with only additive interactions. Because the cell state is modified additively (not multiplicatively), gradients flow backward through it without shrinking exponentially.
Three gates control the cell state. The forget gate decides what to erase from the previous cell state. The input gate decides what new information to write to the cell state. The output gate decides what part of the cell state to expose as the hidden state. All gates output values between 0 and 1 (sigmoid) — 0 means "block completely," 1 means "pass through completely."
PyTorch nn.LSTM — shapes, directions, and layers
PyTorch's nn.LSTM processes an entire sequence in one call. The most important thing to understand is the input and output shapes — they are not intuitive and cause the majority of LSTM bugs. Input is (seq_len, batch, input_size) by default — note seq_len comes first, not batch. Output is the hidden state at every time step plus the final hidden and cell states separately.
LSTM for Amazon session classification — will this user buy?
LSTM for time series — Instacart demand forecasting
Beyond classification, LSTMs are widely used for sequence-to-value regression: given the last N time steps, predict the next value. Instacart predicts hourly demand for each SKU at each dark store — the last 24 hours of sales predict the next hour. This is a many-to-one sequence regression problem.
Every common RNN/LSTM mistake — explained and fixed
Where LSTMs still run in production despite Transformers winning almost everywhere else
For new NLP work — chat, document understanding, code generation, translation — nobody starts a 2026 project with an LSTM. Transformers won that category completely, and reaching for a recurrent architecture there would need a specific justification. But "Transformers won for language" is not the same claim as "LSTMs are obsolete," and a handful of production domains still reach for a recurrent architecture on purpose, for reasons that have nothing to do with which model scores higher on a language benchmark.
The common thread across the domains where LSTMs persist is that they trade raw modelling power for a property Transformers do not have by default: constant memory and compute per new input, regardless of how long the stream has already run. A Transformer's attention has to look back across its full context window for every new token, so serving it for a long-running stream means managing a growing key-value cache. An LSTM just carries forward a fixed-size hidden state and cell state — the thousandth step costs exactly what the first step cost.
Five things people get wrong about RNNs and LSTMs
LSTMs dramatically mitigate vanishing gradients through the additive cell-state update (C = f×C_prev + i×g), which lets gradients flow backward through many time steps without being repeatedly multiplied through a squashing non-linearity — but "mitigate" is not "eliminate." Extremely long sequences can still see gradient decay, since the forget gate itself is a sigmoid that can push toward zero and cut the memory pathway, and exploding gradients remain a live risk through the recurrent weight matrices regardless of gating. That is exactly why gradient clipping is still considered mandatory, standard practice for LSTM training, not an optional safety net — the gates reduce the severity of the problem, they don't make gradient management unnecessary.
Each gate outputs a sigmoid value between 0 and 1 for every dimension of the hidden state, which does make them "gate-like" in spirit, but they are continuously valued and jointly computed from the same shared inputs, not independent binary decisions. A single dimension might have a forget value of 0.3 (mostly, not fully, erase this memory slot) combined with an input value of 0.6 (partially write new information into the same slot) in the same time step — the gates blend old and new information smoothly rather than switching cleanly between "keep everything" and "erase everything." Thinking of them as binary flags misses exactly the mechanism that makes gating powerful: fine-grained, differentiable control over what's remembered.
A GRU merges the LSTM's separate hidden state and cell state into one, and uses two gates (reset and update) instead of three, giving it noticeably fewer parameters and typically faster training and inference per step. Empirically the two often perform comparably on many mid-sized sequence tasks, which is where the "doesn't matter" intuition comes from — but "often comparable" is not "always equivalent." LSTMs' explicit, separate cell-state memory pathway tends to have an edge on tasks with longer-range dependencies, while GRUs' smaller footprint is genuinely preferable when compute- or latency-constrained. Treat the choice as an empirical question for your specific task, not an interchangeable default.
Transformers dominate for tasks where you can afford to process the whole sequence at once and attention's quadratic cost in sequence length is acceptable — but LSTMs remain the better engineering choice in specific, common situations. For streaming or online inference, an LSTM's hidden/cell state gives constant memory and compute per new token, while a Transformer needs to manage a growing context window or KV cache. For very long sequences — tens of thousands of steps, common in genomics or long-form sensor telemetry — full self-attention's quadratic cost can be computationally infeasible where an LSTM's linear cost is not. "Transformers won" for language modelling at scale; that is not a blanket replacement for recurrent architectures in every setting.
A bidirectional LSTM runs one LSTM forward and a second backward over the sequence and concatenates their hidden states — genuinely useful when the full sequence is available upfront and you want each position informed by both what came before and after it. But that requirement makes it structurally unusable for real-time, causal, or streaming prediction: you cannot run the "backward" direction on future tokens that haven't happened yet. For online tasks like next-token prediction, live user-session scoring, or step-by-step time-series forecasting, only a unidirectional LSTM is a valid architecture at all, regardless of any accuracy benefit bidirectionality might offer on paper.
RNNs and LSTMs — 5 questions interviewers actually ask
A vanilla RNN's hidden state is recomputed every step through a tanh non-linearity — backpropagating through time means repeatedly multiplying by tanh's derivative (bounded well under 1 in saturated regions) at every step, so the gradient shrinks exponentially with sequence length. The LSTM's key structural change is the cell state, updated additively — C = f×C_prev + i×g — rather than being pushed back through a squashing function at every step. Because addition, unlike repeated multiplication by sub-1 values, doesn't inherently shrink a gradient, gradients can flow backward through many time steps along the cell-state pathway largely intact, as long as the forget gate stays close to 1 for the relevant span. The gates decide when to use that additive pathway versus when to actually forget — but the additive update itself is what solves the underlying math problem.
With batch_first=True, input is (batch, seq_len, input_size) — matching what a standard DataLoader naturally produces. The call returns output, (h_n, c_n): output has shape (batch, seq_len, hidden_size) and contains the hidden state at every single time step. h_n and c_n, by contrast, are (num_layers, batch, hidden_size) — batch, not seq_len, is not first here even with batch_first=True, because these represent only the final states, indexed by layer rather than by time step. The two most common bugs are forgetting that h_n's first dimension is num_layers, so h_n[-1] gives the last layer's final hidden state, and conflating output[:, -1, :] with h_n[-1] — equal for a single-direction, single-layer LSTM, but they diverge the moment you add layers or bidirectionality.
Three cases come up in practice. First, streaming or real-time inference where you need to process a new data point as it arrives with constant per-step latency and memory — an LSTM's hidden/cell state naturally supports this, while a Transformer needs a growing context window or KV cache. Second, very long sequences — tens of thousands of time steps, common in genomics or extended time series — where full self-attention's quadratic cost becomes computationally prohibitive and an LSTM's linear cost is the pragmatic choice. Third, resource-constrained deployment, where a small LSTM is simply cheaper to run than even a modest Transformer. For most NLP and any task with abundant compute and the full sequence available upfront, Transformers remain the default.
A feedforward network's gradient flows through depth — a fixed, architecturally-bounded number of layers. A recurrent network's gradient flows through both depth and time: processing a sequence of length 100 is, from backprop's perspective, equivalent to a 100-layer-deep unrolled network, and that unrolled depth is determined by your data, not a fixed architectural choice. This makes exploding gradients — where the same recurrent weight matrix, applied repeatedly, amplifies the gradient each step — a much more common and less predictable failure mode for RNNs/LSTMs than for a network with a handful of fixed layers. Gradient clipping rescales the gradient vector when its norm exceeds a threshold, cheap insurance applied by default in essentially every serious RNN/LSTM training loop, independent of whether NaNs have actually been observed yet.
For a single-layer, unidirectional LSTM, h_n[-1] and output[:, -1, :] contain identical values — both are the hidden state at the final time step. They diverge the moment you add multiple layers or bidirectionality. With multiple layers, output only ever reflects the top layer's hidden state at each time step, while h_n contains the final hidden state of every layer stacked — so h_n[-1] is what matches output[:, -1, :], and grabbing the wrong index silently gives you an intermediate layer's representation instead. With bidirectionality it's worse: h_n's last two entries are the final forward-direction and final backward-direction states, and the backward direction's "final" state actually corresponds to the first time step of the sequence — using output[:, -1, :] directly in that setting mixes a real forward-final-step signal with the backward pass's very first computed state, a substantive and easy-to-miss correctness bug.
You can model sequences. Next: the architecture that replaced RNNs for almost everything.
LSTMs process sequences step by step — they cannot parallelise across time steps during training. A sequence of 512 tokens requires 512 sequential LSTM steps. Transformers replaced this with self-attention — every token attends to every other token simultaneously. Training is fully parallelisable, long-range dependencies are captured in a single layer, and the results are dramatically better. Every modern LLM — GPT, Gemini, Claude — is a Transformer. Module 48 builds self-attention from scratch.
Queries, keys, values, and why attention is all you need. Build a self-attention layer from scratch, then see how GPT and BERT use it.
🎯 Key Takeaways
- ✓RNNs process sequences by maintaining a hidden state — a vector summarising everything seen so far. At each step: hₜ = tanh(Wₓxₜ + Wₕhₜ₋₁ + b). The same weights are reused at every step (weight sharing across time). The final hidden state represents the entire sequence.
- ✓The vanishing gradient problem: tanh derivatives are at most 1. Over 50 time steps, gradients shrink by 0.7⁵⁰ ≈ 0.0000001. Early time steps receive essentially zero gradient — the network cannot learn long-range dependencies.
- ✓LSTMs add a cell state Cₜ alongside the hidden state hₜ. The cell state is updated additively: C = f × C_prev + i × g. Additive updates allow gradients to flow backward without shrinking — this is why LSTMs can learn dependencies 100+ steps apart.
- ✓Three gates control the cell state: forget gate f (what to erase from memory), input gate i + candidate g (what new information to write), output gate o (what part of memory to expose as hidden state). All gates use sigmoid — values between 0 and 1 act as soft on/off switches.
- ✓PyTorch LSTM shapes: input is (batch, seq_len, input_size) with batch_first=True. output is (batch, seq_len, hidden_size) — hidden at every step. h_n is (num_layers, batch, hidden_size) — final hidden. Always use pack_padded_sequence for variable-length sequences. Always clip gradients: nn.utils.clip_grad_norm_(model.parameters(), 1.0).
- ✓Use LSTMs for: time series forecasting (demand, sensor readings), sequence classification (session prediction, sentiment), anomaly detection in sequential data. For new NLP projects use Transformers (Module 48) — LSTMs are the standard choice only for time series and very long sequences where attention would be prohibitively expensive.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.