Long Short-Term Memory
Give the network a separate memory line that gates control instead of overwrite, so relevant information from many steps back can survive untouched until it's needed.
Why Does This Exist?
A vanilla RNN hits a specific wall: its hidden state gets fully recomputed at every step through a nonlinearity, and the gradient that would teach it to remember something from 50 steps back has to survive 50 multiplications on the way there. In practice it doesn't. Ask a plain RNN to recall the subject of a sentence when it finally reaches the verb, twenty words later, and it usually can't — the information is either overwritten or the gradient carrying "this mattered" has decayed to nothing before training ever sees it.
The fix that shipped in 1997 didn't try to make the existing hidden-state update numerically nicer. It restructured the network around a second piece of state whose entire job is not getting squashed through a nonlinearity every step, and whose contents are edited deliberately rather than overwritten wholesale.
Think of It Like This
A whiteboard with an eraser, a marker, and a window
Picture a running project whiteboard. Most of what's written stays untouched for a long time — it's not rewritten from scratch at every meeting. Three small actions happen instead: someone erases the lines that are no longer relevant (forget), someone adds new notes for what just came up (input), and someone glances at the board to decide what to actually say out loud in this meeting (output) — which is usually a filtered version of what's written, not the whole board.
The whiteboard itself — the cell state — persists unchanged except where one of those three actions deliberately touches it. That's the entire difference from a vanilla RNN, where the whole board gets erased and rewritten every single step whether anything needed to change or not.
How It Actually Works
Three gates, one memory line
The diagram above shows the cell state running straight across, edited by exactly three gates. Each gate is a small feedforward layer — a sigmoid over — producing a vector of values between 0 and 1, one per memory dimension, that acts as a per-dimension "how much."
The forget gate decides what to erase:
A value near 0 in some dimension erases that dimension of the cell state; near 1 keeps it. This gate is multiplicative — it scales the existing memory down, never adds anything.
The input gate decides what to write, working alongside a candidate value :
The cell state update combines both gates in one line:
( is elementwise multiplication.) Forget scales down what's already there; input adds a new, gated candidate on top. Nothing here forces a full rewrite — if and everywhere, , and the memory survives this step completely untouched.
The output gate decides what the rest of the network actually sees this step:
is the hidden state that feeds the next layer or a prediction — a filtered, squashed view of the cell state, not the cell state itself.
Why this actually fixes the gradient problem
Look at the cell-state update again: . The path from to is a multiplication by — a learned, per-dimension gate — not a matrix multiply chained through the way a vanilla RNN's hidden state is. When the network learns for a dimension worth preserving, the gradient through that dimension across that step is also close to 1. Chain a hundred steps where and the gradient survives a hundred steps, because it was never forced through a saturating nonlinearity along the way. The forget gate isn't just a memory-control knob — it's simultaneously a gradient-control knob, and that's the actual mechanism behind "long" in the name.
Show Me the Code
One LSTM cell step, with the four sub-computations kept visible rather than fused into a single matrix multiply.
import numpy as np
def sigmoid(z: np.ndarray) -> np.ndarray: return 1.0 / (1.0 + np.exp(-z))
def lstm_step( x_t: np.ndarray, h_prev: np.ndarray, c_prev: np.ndarray, w: dict[str, np.ndarray]) -> tuple[np.ndarray, np.ndarray]: z = np.concatenate([x_t, h_prev]) # combined input, fed to every gate f_t = sigmoid(w["f"] @ z) # forget gate: how much of c_prev survives i_t = sigmoid(w["i"] @ z) # input gate: how much of the candidate to add c_tilde = np.tanh(w["c"] @ z) # candidate memory content o_t = sigmoid(w["o"] @ z) # output gate: how much of c_t becomes h_t c_t = f_t * c_prev + i_t * c_tilde h_t = o_t * np.tanh(c_t) return h_t, c_t
rng = np.random.default_rng(0)weights = {k: rng.normal(scale=0.3, size=(4, 6)) for k in ("f", "i", "c", "o")}h, c = np.zeros(4), np.zeros(4)h, c = lstm_step(np.array([1.0, 0.0]), h, c, weights)print(np.round(c, 3)) # -> [ 0.062 -0.02 -0.031 0.056]The f_t * c_prev term is the one line to remember. Set every entry of f_t to 1 and every entry of i_t to 0, and c_t equals c_prev exactly — untouched memory, by construction.
Watch Out For
Initializing the forget gate to erase by default
With weights initialized near zero, — the cell state decays by half at every step before the network has learned anything. Some implementations initialize the forget-gate bias to a small positive value (around 1) specifically so training starts closer to "remember by default" rather than "forget by default," which measurably speeds up learning long-range dependencies. Check your framework's default before assuming it's neutral.
Confusing the cell state with the hidden state
and are two different objects — the cell state is the long-lived memory line, the hidden state is what's actually passed to the next layer or used for prediction at this step. A common bug is feeding where was expected (or vice versa) when wiring a custom LSTM by hand. They have the same shape, so nothing crashes; the network just trains worse, quietly, because the output gate's filtering never happens.
The Quick Version
- An LSTM adds a separate cell state alongside the hidden state, edited by three sigmoid gates instead of fully overwritten each step.
- The forget gate scales existing memory down; the input gate adds a new gated candidate; the output gate filters what the rest of the network sees.
- The cell-state update is additive and gate-controlled, not a full nonlinear rewrite — that's what lets gradients survive many steps.
- A forget gate near 1 for a dimension means both "keep this" and "the gradient through this dimension survives," which is the actual fix for vanishing gradients.
- Cell state and hidden state are different objects with different jobs — don't conflate them.
What to Read Next
- Vanilla RNN is the architecture whose vanishing-gradient wall this page's gates exist to fix.
- Gated Recurrent Unit merges the cell and hidden states into one and drops to two gates, for a cheaper version of the same idea.
- Bidirectional RNNs pair naturally with an LSTM cell when the whole sequence is available up front.
- Residual Connections solve a related gradient-flow problem in feedforward networks with the same "add instead of overwrite" instinct.
- Backpropagation is the mechanism whose long-chain multiplication this page's gating strategy is designed around.