LSTM and GRU
Build an LSTM cell gate-by-gate; show how the constant error carousel defeats vanishing gradients; then simplify to the GRU.
The vanilla RNN multiplies the hidden state by at every step, which either shrinks gradients exponentially (vanishing) or explodes them. The Long Short-Term Memory (LSTM) introduces a cell state that flows through the network with only additive updates -- gradients can travel backward through time without repeated matrix multiplication.
The LSTM cell
The LSTM (Hochreiter and Schmidhuber, 1997)[1] maintains two vectors per time step:
- : the cell state (long-term memory).
- : the hidden state (output passed to the next layer).
Four gates control the information flow. All gate activations use the same weight matrices applied to the concatenated input :
Cell and hidden state updates:
denotes elementwise multiplication.
The constant error carousel
The cell update is additive in . The gradient of with respect to is:
This is a diagonal matrix -- no matrix multiplication through . If (the gate is open), gradients flow backward unchanged. Hochreiter called this the constant error carousel: errors are "carried" through time at constant scale.
In practice, the forget gate is initialized with a positive bias () so that it starts close to 1, ensuring gradients flow freely at the beginning of training.
LSTM implementation
import numpy as np
class LSTMCell:
def __init__(self, d, m, seed=0):
rng = np.random.default_rng(seed)
scale = 0.1
concat_dim = d + m
# All four gates in one matrix for efficiency
self.W = rng.normal(0, scale, (4 * m, concat_dim))
self.b = np.zeros(4 * m)
self.b[0:m] = 1.0 # forget gate bias init = 1
self.m = m
def forward(self, x, h_prev, c_prev):
"""Single LSTM step. Returns (h, c)."""
m = self.m
combined = np.concatenate([h_prev, x])
gates = self.W @ combined + self.b # (4m,)
f = self._sigmoid(gates[0 : m]) # forget
i = self._sigmoid(gates[m : 2*m]) # input
g = np.tanh (gates[2*m : 3*m]) # candidate
o = self._sigmoid(gates[3*m : 4*m]) # output
c = f * c_prev + i * g
h = o * np.tanh(c)
return h, c, (x, h_prev, c_prev, f, i, g, o, c)
@staticmethod
def _sigmoid(x):
return 1.0 / (1.0 + np.exp(-np.clip(x, -10, 10)))
def run_lstm(cell, xs):
"""xs: (T, d) array. Returns all hidden states (T, m)."""
m = cell.m
h, c = np.zeros(m), np.zeros(m)
hs = []
for x in xs:
h, c, _ = cell.forward(x, h, c)
hs.append(h)
return np.array(hs)
d, m = 4, 8
cell = LSTMCell(d, m)
xs = np.random.randn(10, d) # 10-step sequence
hs = run_lstm(cell, xs)
print("Hidden states shape:", hs.shape)
print("Last hidden state norm:", np.linalg.norm(hs[-1]))Gated Recurrent Unit (GRU)
The GRU (Cho et al., 2014)[2] is a simplified LSTM that merges cell and hidden states and uses two gates instead of four:
The update gate interpolates between the previous hidden state and the new candidate:
- : carry the old state forward (remember).
- : replace with the new candidate (update).
The reset gate modulates how much the previous state influences the candidate.
import numpy as np
class GRUCell:
def __init__(self, d, m, seed=1):
rng = np.random.default_rng(seed)
s = 0.1
cdim = d + m
self.Wr = rng.normal(0, s, (m, cdim))
self.Wz = rng.normal(0, s, (m, cdim))
self.Wh = rng.normal(0, s, (m, cdim))
self.m = m
def forward(self, x, h_prev):
combined = np.concatenate([h_prev, x])
r = self._sigmoid(self.Wr @ combined)
z = self._sigmoid(self.Wz @ combined)
h_tilde = np.tanh(self.Wh @ np.concatenate([r * h_prev, x]))
h = (1 - z) * h_prev + z * h_tilde
return h
@staticmethod
def _sigmoid(x):
return 1.0 / (1.0 + np.exp(-np.clip(x, -10, 10)))
gru = GRUCell(d=4, m=8)
xs = np.random.default_rng(0).normal(size=(10, 4)) # 10-step sequence
h = np.zeros(8)
for x in xs:
h = gru.forward(x, h)
print("GRU final hidden norm:", np.linalg.norm(h))LSTM vs GRU vs vanilla RNN
| Vanilla RNN | GRU | LSTM | |
|---|---|---|---|
| Parameters | |||
| Memory | only | only | + |
| Long-range gradients | Vanishes | Good | Excellent |
| Speed | Fastest | Fast | Slower |
Exercises
Exercise 1. Take a one-dimensional LSTM () at a single step with gate values , , , and previous cell state . Compute and by hand using and .
Solution
Step 1. Update the cell state: . The forget gate keeps of the old cell, the input gate writes of the candidate.
Step 2. Squash the cell state: .
Step 3. Apply the output gate: . The hidden state is a gated, bounded readout of the cell, while the cell itself accumulated additively.
Exercise 2. The gradient of the cell state across one step is . Show that the gradient carried across steps is (elementwise), and contrast its decay with the vanilla RNN's .
Solution
Step 1. Apply the chain rule along the cell-state path. Because each step contributes the diagonal factor ,
a product of diagonal matrices, which is elementwise multiplication of the forget gates.
Step 2. No appears in this product. When for all , the product stays near and the gradient is preserved across the whole sequence, the constant error carousel.
Step 3. The vanilla RNN instead multiplies by full matrices at every step, whose norm shrinks like (with the derivative shrinking it further). The LSTM replaces that contractive matrix product with a near-identity scalar product, which is why long-range gradients survive.
Exercise 3 (code). Implement the cell-state gradient path for a constant forget gate, and confirm that preserves far more gradient over steps than .
Solution
import numpy as np
m, T = 5, 30
def carousel_grad(f_value):
forgets = np.full((T, m), f_value) # forget gate at each step
return np.prod(forgets, axis=0) # dc_T/dc_0 = elementwise product
g_open = carousel_grad(0.95) # gate near 1: memory persists
g_half = carousel_grad(0.50) # gate at 0.5: memory decays fast
assert np.allclose(g_open, 0.95 ** T) # exact product
assert np.all(g_half < g_open) # smaller forget gate vanishes faster
print("f=0.95 ->", round(g_open[0], 4),
" f=0.50 ->", round(g_half[0], 12))With the surviving gradient is , while collapses to : an open forget gate carries signal across the sequence, a half-open one destroys it.
GRU matches LSTM on most benchmarks with fewer parameters; LSTM tends to win on very long sequences where the separate cell state helps. Both are superseded by Transformers for most tasks, but appear as components in sequence models and state-space models.
References
- [1]Hochreiter, S., & Schmidhuber, J. (1997). "Long short-term memory". Neural Computation, 9(8), 1735-1780. doi:10.1162/neco.1997.9.8.1735. The cell state and constant error carousel.↩
- [2]Cho, K., et al. (2014). "Learning phrase representations using RNN encoder-decoder for statistical machine translation". EMNLP. arXiv:1406.1078. Introduces the GRU.↩