LSTM: gated memory

LSTM: gated memory

30 min read

The previous lesson, on the vanishing gradient, closed with a diagnosis and a direction. The diagnosis: as long as every step of the backward pass goes through the same matrix, the result is a power of the distance. The direction: a state that moves forward as a running sum, and the network built around one, the long short-term memory (LSTM). But naming it isn't building it. A memory that only adds has a defect as serious as the one it fixes: it never erases anything. What's left is the hard part, deciding step by step what to keep, what to write over it, and how much of what's stored to let out.

An ordinary sentence shows that these are three decisions, not one. Take The cameras that the mayor installed in the square last year are broken. To write are rather than is, the network has to carry the plural of cameras across ten positions, without mayor or square (both singular, both closer) overwriting it along the way. Carrying it intact is one thing; storing it when cameras appears, and not at every passing word, is another; and keeping it stored through those ten positions without letting it drive the network's answers at the positions in between is a third. The vanilla recurrent neural network (RNN) handles all three with the same move, because it only has one: rewriting the whole of ht\mathbf{h}_t at every step. That's why it can't do any of them well.

Separating the three jobs starts with separating two states. Alongside the ht\mathbf{h}_t you already know, a second vector appears, the cell state ct∈Rdh\mathbf{c}_t \in \mathbb{R}^{d_h}, which is the memory proper, and the way it moves forward is the only genuinely new idea in this lesson. Everything else in the LSTM exists to control that vector.

Three gates, each fixing a failure of the previous version

Start from the additive path on its own and add what it's missing as we go. The candidate is the vector that step tt proposes to store, computed by an ordinary layer from the token and the previous state:

c~t=tanh⁡(Wxcxt+Whcht−1+bc)∈Rdh,\tilde{\mathbf{c}}_t = \tanh\left(\mathbf{W}_{xc}\mathbf{x}_t + \mathbf{W}_{hc}\mathbf{h}_{t-1} + \mathbf{b}_c\right) \in \mathbb{R}^{d_h},

and let the memory accumulate it: ct=ct−1+c~t\mathbf{c}_t = \mathbf{c}_{t-1} + \tilde{\mathbf{c}}_t. For the gradient this is perfect: the derivative of ct\mathbf{c}_t with respect to ct−1\mathbf{c}_{t-1} is the identity, with no matrix applied dd times. As a memory, though, it's useless, for two reasons. It never forgets: whatever went into coordinate 3 while reading cameras is still there three hundred tokens later, in a paragraph about something else entirely. And it grows without limit: each term lives in (−1,1)(-1, 1) and TT of them get added, so a coordinate can end up at 4040, where the tanh⁡\tanh reading it is completely flat. That's the saturation you already saw in the explorable in the lesson on activation functions.

The two failures are really one: the sum has no way of saying no. Give it one, in the form of a factor per coordinate between 00 and 11 that the network computes at every step from what it's reading. That's the forget gate:

ft=σ(Wxfxt+Whfht−1+bf)∈(0,1)dh,ct=ft⊙ct−1+c~t.\mathbf{f}_t = \sigma\left(\mathbf{W}_{xf}\mathbf{x}_t + \mathbf{W}_{hf}\mathbf{h}_{t-1} + \mathbf{b}_f\right) \in (0,1)^{d_h}, \qquad \mathbf{c}_t = \mathbf{f}_t \odot \mathbf{c}_{t-1} + \tilde{\mathbf{c}}_t.

The activation is the logistic sigmoid rather than the candidate's tanh⁡\tanh, for the same reason as the output layer in the lesson on the vanilla RNN: what's needed is a fraction of what was there, and fractions run from 00 to 11. The ⊙\odot deserves an equally careful reading. Multiplying coordinate by coordinate lets coordinate 3 be kept whole while coordinate 7 is erased in the same step; a matrix would mix the two, and mixing is the last thing a memory wants.

That leaves the other side. This version writes c~t\tilde{\mathbf{c}}_t at every step, whatever the token is worth: that and the write as much as cameras, and the only way to undo what's been written is to close ft\mathbf{f}_t, which also wipes out whatever that coordinate was already holding. Making the writing conditional takes another factor of the same kind, the input gate:

it=σ(Wxixt+Whiht−1+bi),ct=ft⊙ct−1+it⊙c~t.\mathbf{i}_t = \sigma\left(\mathbf{W}_{xi}\mathbf{x}_t + \mathbf{W}_{hi}\mathbf{h}_{t-1} + \mathbf{b}_i\right), \qquad \mathbf{c}_t = \mathbf{f}_t \odot \mathbf{c}_{t-1} + \mathbf{i}_t \odot \tilde{\mathbf{c}}_t.

That leaves the question of what the rest of the network gets to see. If the hidden state were ht=tanh⁡(ct)\mathbf{h}_t = \tanh(\mathbf{c}_t) (all of the memory, all of the time), the plural of cameras would drive the answer throughout those ten positions, when what's needed is for it to wait. Keeping something for later and not using it now are two different things, so there's a third gate, the output gate, which makes the hidden state a partial view of the memory:

ot=σ(Wxoxt+Whoht−1+bo),ht=ot⊙tanh⁡(ct).\mathbf{o}_t = \sigma\left(\mathbf{W}_{xo}\mathbf{x}_t + \mathbf{W}_{ho}\mathbf{h}_{t-1} + \mathbf{b}_o\right), \qquad \mathbf{h}_t = \mathbf{o}_t \odot \tanh\left(\mathbf{c}_t\right).

That's what the vanilla RNN couldn't do: its ht\mathbf{h}_t had two jobs at once, remembering and showing. The LSTM splits them: ct\mathbf{c}_t remembers, and ht\mathbf{h}_t shows.

Putting the four pieces together (three gates and a candidate), the full step looks like this, with h0=c0=0\mathbf{h}_0 = \mathbf{c}_0 = \mathbf{0} for the same reason as in the RNN:

ft=σ(Wxfxt+Whfht−1+bf),it=σ(Wxixt+Whiht−1+bi),ot=σ(Wxoxt+Whoht−1+bo),c~t=tanh⁡(Wxcxt+Whcht−1+bc),ct=ft⊙ct−1+it⊙c~t,ht=ot⊙tanh⁡(ct).\begin{aligned} \mathbf{f}_t &= \sigma\left(\mathbf{W}_{xf}\mathbf{x}_t + \mathbf{W}_{hf}\mathbf{h}_{t-1} + \mathbf{b}_f\right), & \mathbf{i}_t &= \sigma\left(\mathbf{W}_{xi}\mathbf{x}_t + \mathbf{W}_{hi}\mathbf{h}_{t-1} + \mathbf{b}_i\right), \\ \mathbf{o}_t &= \sigma\left(\mathbf{W}_{xo}\mathbf{x}_t + \mathbf{W}_{ho}\mathbf{h}_{t-1} + \mathbf{b}_o\right), & \tilde{\mathbf{c}}_t &= \tanh\left(\mathbf{W}_{xc}\mathbf{x}_t + \mathbf{W}_{hc}\mathbf{h}_{t-1} + \mathbf{b}_c\right), \\ \mathbf{c}_t &= \mathbf{f}_t \odot \mathbf{c}_{t-1} + \mathbf{i}_t \odot \tilde{\mathbf{c}}_t, & \mathbf{h}_t &= \mathbf{o}_t \odot \tanh\left(\mathbf{c}_t\right). \end{aligned}
One step of an LSTM. A green line, the cell state, runs across the top from side to side and meets only two circles along the way: one for a product and one for a sum. Below it sit four small boxes labelled f, i, c with a tilde, and o, all fed by the same dotted line, which carries the previous hidden state and the token's vector. Box f feeds up into the product circle on the top line; i and c with a tilde first meet in another product circle below the line, and only its output rises to the sum. On the right, a green branch drops from the top line, passes through a tanh and meets box o in a third product circle, whose output is the only line labelled as the new hidden state. No line connects the previous hidden state directly to the new one.
The six equations above, drawn out: the cell state crosses the step from side to side without being multiplied by any matrix; one gate scales it and another meters what gets added to it. That's the path the gradient will be able to travel back along, and the hidden state branches off it instead of replacing it.

Four of those equations share one form, and two don't. The top four are the recurrence from the lesson on the vanilla RNN copied four times, each with Wx∗∈Rdh×dmodel\mathbf{W}_{x\ast} \in \mathbb{R}^{d_h \times d_{\text{model}}}, Wh∗∈Rdh×dh\mathbf{W}_{h\ast} \in \mathbb{R}^{d_h \times d_h} and b∗∈Rdh\mathbf{b}_{\ast} \in \mathbb{R}^{d_h}, and there's the price: the LSTM costs 4(dh⋅dh+dh⋅dmodel+dh)4\left(d_h \cdot d_h + d_h \cdot d_{\text{model}} + d_h\right) weights, four times as many as the vanilla RNN. What it doesn't cost is length: here too, TT appears in no shape.

Why the gradient survives the additive path

The claim that the cell "adds" has come up three times now; time to cash it in. Look at the second-to-last equation coordinate by coordinate, which is where the meaning of the ⊙\odot becomes visible:

ct,j=ft,j ct−1,j+it,j c~t,j.c_{t,j} = f_{t,j}\,c_{t-1,j} + i_{t,j}\,\tilde{c}_{t,j}.

Coordinate jj of the new cell depends on coordinate jj of the old one and on no other: along this path the coordinates don't mix, because there's no matrix to mix them. Differentiating while holding the gates still (I'll come back to this), the direct path from one step to the one before is worth

∂ct,j∂ct−1,j=ft,j,\frac{\partial c_{t,j}}{\partial c_{t-1,j}} = f_{t,j},

a number, not a matrix. Chain it from the end back to a distant step kk, the way the previous lesson chained the backward recurrence, and what reaches the cell at kk along that path is

∂ℓ∂ck,j=(∏t=k+1Tft,j)∂ℓ∂cT,j+(what comes in through the gates).\frac{\partial \ell}{\partial c_{k,j}} = \left(\prod_{t=k+1}^{T} f_{t,j}\right)\frac{\partial \ell}{\partial c_{T,j}} + \left(\text{what comes in through the gates}\right).

Set that beside the previous lesson's (γ σmax⁡)T−k\left(\gamma\,\sigma_{\max}\right)^{T-k} and count the differences. There are three. First, the base is no longer a property of one matrix shared by everything, but a number per coordinate and per step that the network computes from what it's reading. Second, there's no tanh⁡\tanh mask biting at every step along this path (no γ≤1\gamma \le 1 that can only make things worse), because the cell crosses each step with a product and a sum and nothing else. And the third clinches it: ft,j=1f_{t,j} = 1 is both reachable and stable, because a saturated sigmoid sits at 11 with almost zero slope, so a coordinate the network learns to keep open multiplies by 11 as many times as needed. The RNN's ρ=1\rho = 1 was a knife-edge shared by every coordinate; this is a decision each coordinate makes for itself.

See what "holding the gates still" left out

ft\mathbf{f}_t, it\mathbf{i}_t and c~t\tilde{\mathbf{c}}_t are computed from ht−1\mathbf{h}_{t-1}, and ht−1=ot−1⊙tanh⁡(ct−1)\mathbf{h}_{t-1} = \mathbf{o}_{t-1} \odot \tanh(\mathbf{c}_{t-1}) depends on ct−1\mathbf{c}_{t-1}. So the full derivative adds, to the ft,jf_{t,j} above, the paths that run through the three gates and through the candidate, and every one of them crosses a Wh∗\mathbf{W}_{h\ast} and an activation mask: they behave like the paths in the previous lesson, and they vanish the same way.

Their vanishing breaks nothing, and that's the part worth thinking about. Those paths adjust how the decision is made at each step, and a decision at this step relies on what's nearby; the signal that has to arrive from far away travels the additive path, and that path crosses no matrix. The LSTM doesn't eliminate the previous lesson's product: it moves it off the long path and leaves it on the short ones.

Two concessions close the account, because without them this would read as a guarantee. If the network learns ft,jf_{t,j} around 0.70.7, the product sinks as it did before; the difference is that it sinks because the network chose that, not because the architecture forces it. And explosion is still possible along the paths in the box above, so the previous lesson's gradient clipping stays in use, unchanged.

The LSTM, step by step, in NumPy

Two cells. The first transcribes the six-equation block and runs it on a short sequence; the second measures how much of a nudge given at the start of a long sequence survives to the end.

I draw the weights from a generator with a fixed seed, and they're untrained, so the gates don't mean anything yet. What I did set by hand is the forget gate's bias, and the cell runs with two different values of it: it's the quickest way to see that bf\mathbf{b}_f decides how much the network remembers before it has learned anything. Look at the last line of each block.

import numpy as np

d_model, d_h, T = 4, 3, 6
X = np.random.default_rng(1).normal(size=(T, d_model)) * 0.5


def sigmoid(z):
return 1.0 / (1.0 + np.exp(-z))


def weights(forget_bias):
rng = np.random.default_rng(0) # the same weights in both cases
return {k: (rng.normal(size=(d_h, d_model)) * 0.5,
rng.normal(size=(d_h, d_h)) * 0.5,
np.full(d_h, b))
for k, b in [("f", forget_bias), ("i", 0.0), ("o", 0.0), ("c", 0.0)]}


def step(P, h, c, x):
def pre(k):
Wx, Wh, b = P[k]
return Wx @ x + Wh @ h + b
f, i, o = sigmoid(pre("f")), sigmoid(pre("i")), sigmoid(pre("o"))
c = f * c + i * np.tanh(pre("c")) # the additive path, in one line
return o * np.tanh(c), c, f, i, o


for bias in [1.0, 3.0]:
P = weights(bias)
h, c, forgets = np.zeros(d_h), np.zeros(d_h), []
print("b_f = %.0f" % bias)
print(" t f_t i_t o_t c_t")
for t, x in enumerate(X, 1):
h, c, f, i, o = step(P, h, c, x)
forgets.append(f)
print(" %d %s %s %s %s" % (t, np.round(f, 2), np.round(i, 2),
np.round(o, 2), np.round(c, 2)))
print(" what survives of c_1 in c_6:", np.round(np.prod(forgets[1:], axis=0), 3))
numpy

The first run downloads the Python interpreter (~15 MB). After that it stays in the browser cache and is reused across every lesson.

The three gates live in (0,1)(0,1) and each takes a different value in every coordinate, which is all the formula promised. Now compare the two blocks. With bf=1\mathbf{b}_f = 1, the forget gate hovers around 0.70.7 and about 20%20\% of c1\mathbf{c}_1 makes it to c6\mathbf{c}_6; with bf=3\mathbf{b}_f = 3, it sits near 0.950.95 and 78%78\% gets through, with the same weights and the same sequence. Five steps are enough for a bias to make the difference between a memory and a sieve, hence the habit of initialising bf\mathbf{b}_f to a positive value: it's cheaper for the network to learn to forget than to learn to remember. The first row of ct\mathbf{c}_t, on the other hand, is identical in both blocks: at t=1t = 1 the gate multiplies c0=0\mathbf{c}_0 = \mathbf{0}.

Try that same bias yourself. In the explorable below you set bf\mathbf{b}_f by hand and follow every coordinate over another six-step sequence, plus one extra row for the product the previous section derived, ∏t=k+1Tft,j\prod_{t=k+1}^{T} f_{t,j}: how much of c1\mathbf{c}_1 survives at each step that follows. The hidden state ht\mathbf{h}_t stays out of view again, as in the cell above, because what matters here is the additive path of ct\mathbf{c}_t.

Raise the forget-gate bias and the gate opens towards 1; step through the sequence and watch the last row, which shows how much of c₁ survives. With a high bias the memory is kept almost whole; with a low one it leaks away to almost nothing, with the same weights and the same sequence.

The second cell measures the backward pass without differentiating anything: it nudges the first step's state by a tiny amount, lets thirty-nine more steps run, and compares. It's the numerical probe from the lesson on backpropagation through time (BPTT), applied to a distance instead of a weight. The gates in this toy read only the token (I disconnected the state by hand), so that the cell measures the additive path and nothing else.

import numpy as np

T, eps = 40, 1e-6
X = np.random.default_rng(2).normal(size=T) * 0.5 # one coordinate, T steps


def sigmoid(z):
return 1.0 / (1.0 + np.exp(-z))


def rnn(nudge): # h_t = tanh(w h_{t-1} + u x_t)
h = 0.0
for t, x in enumerate(X):
h = np.tanh(0.9 * h + 0.5 * x)
if t == 0:
h += nudge # nudge the state of step 1
return h


def lstm(nudge, b_f): # c_t = f_t c_{t-1} + i_t c~_t
c = 0.0
for t, x in enumerate(X):
c = sigmoid(0.5 * x + b_f) * c + sigmoid(0.5 * x) * np.tanh(0.5 * x)
if t == 0:
c += nudge # nudge the cell of step 1
return c


print("of a nudge given at step 1, how much is left 39 steps later:")
print(" RNN, w = 0.9 %.2e" % abs((rnn(eps) - rnn(0.0)) / eps))
for b_f in [0.0, 3.0, 5.0]:
q = abs((lstm(eps, b_f) - lstm(0.0, b_f)) / eps)
print(" LSTM, b_f = %.0f %.2e (f_t around %.3f)" % (b_f, q, sigmoid(b_f)))
numpy

The first run downloads the Python interpreter (~15 MB). After that it stays in the browser cache and is reused across every lesson.

Four numbers, three readings. From the RNN, 1.1×10−31.1 \times 10^{-3} gets through: the matrix accounts for 0.939≈0.0160.9^{39} \approx 0.016 and the tanh⁡\tanh mask for the rest (another order of magnitude), the γ≤1\gamma \le 1 that the previous lesson said could only make things worse. The second line shows the LSTM coming out worse than the RNN: with bf=0\mathbf{b}_f = 0 the gate hovers around 0.50.5, and thirty-nine factors like that leave about 0.539≈2×10−120.5^{39} \approx 2 \times 10^{-12} of the nudge, too little for floating-point arithmetic to register, so the line prints a flat zero. This memory is worse than the one it was meant to fix. The additive path is no free gift; all it does is make reachable what was out of reach in the RNN. The last two lines are what "reachable" means: with the gate at 0.950.95, 14%14\% survives, and at 0.9930.993, 76%76\% does, over the same distance at which the RNN was down to three zeros after the decimal point.

Test your intuition

Four questions: why the additive path changes the product, what each gate does in a specific case, how much the architecture costs, and what the LSTM still doesn't guarantee.

Why doesn't an LSTM's cell state doom the gradient to the previous lesson's exponential power?

One coordinate of the cell state records that the subject of the sentence was plural. It has to be kept through an entire relative clause, but without influencing the network's answer at the positions in between. What do that coordinate's gates do for the length of the relative clause?

Take an LSTM with a state of dh=64d_h = 64 coordinates reading embeddings of dmodel=128d_{\text{model}} = 128. How many weights do its eight matrices and four biases add up to?

A margin of ±0 is accepted.

Tick everything the LSTM does not guarantee.

Select every correct option. This is graded all-or-nothing: there is no partial credit.

And then the full step, written by you: the piece everything that follows is built on.

Write one LSTM step: lstm_step(h, c, x, P) takes the previous hidden state and cell state, the current token's vector, and a dictionary P holding the weights of the four pieces (P["f"], P["i"], P["o"] and P["c"], each a triple (Wx, Wh, b)). It returns the pair (h_new, c_new), in that order.

All four are computed the same way, Wx∗xt+Wh∗ht−1+b∗\mathbf{W}_{x\ast}\mathbf{x}_t + \mathbf{W}_{h\ast}\mathbf{h}_{t-1} + \mathbf{b}_{\ast}; only the activation changes: σ\sigma for the three gates, tanh⁡\tanh for the candidate. Don't write into the arrays you receive.

The first run downloads the Python interpreter (~15 MB); after that it stays in the browser cache. This challenge is much easier to solve on a physical keyboard: on a phone, read it and come back later.


The LSTM solves the previous lesson's problem, but at a price the weight count only half captures. Eight matrices, two states to carry and three gates to tune: each piece arrived to fix a specific failure, true, but none came with a proof that it was necessary. And at least one of them looks suspicious. The input gate decides how much to write at the very moment the forget gate has decided how much to keep, and those are really one question seen from two sides: if you're going to store something new, it's because the old matters less to you.

Someone pulled on that thread. Merging those two gates into one, dropping the output gate along with the whole cell state, and getting by with three quarters of the weights, while holding on to almost everything this lesson has gained, is the subject of the next lesson, on the gated recurrent unit (GRU): the simplified version.

Was this helpful?
Further reading3 sources · 2 papers, 1 article

Where this lesson comes from, and where to go next. None of it is needed to carry on with the course.

  • Long Short-Term Memory
    paperHochreiter and Schmidhuber, 1997Neural Computation 9(8)EN

    The paper that introduced it. It has the cell state and the constant error flow running through it (your additive path), but only input and output gates: the forget gate was added by Gers, Schmidhuber and Cummins in 2000.

  • Understanding LSTM Networks
    articleChristopher Olah, 2015colah.github.ioEN

    Diagrams of the three gates and of the cell state as a "conveyor belt" (this lesson's figure), with the GRU introduced along the way. It explains why the additive path helps the gradient but doesn't derive it; you've already done that.

  • LSTM: A Search Space Odyssey
    paperGreff, Srivastava, Koutník, Steunebrink and Schmidhuber, 2017arXiv:1503.04069EN

    Eight LSTM variants across 5,400 training runs: what really matters is the forget gate and the output activation, while coupling the input and forget gates, as the GRU will, barely changes anything. No variant beats the standard one.