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 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 you already know, a second vector appears, the cell state , 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 proposes to store, computed by an ordinary layer from the token and the previous state:
and let the memory accumulate it: . For the gradient this is perfect: the derivative of with respect to is the identity, with no matrix applied 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 and of them get added, so a coordinate can end up at , where the 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 and that the network computes at every step from what it's reading. That's the forget gate:
The activation is the logistic sigmoid rather than the candidate's , 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 to . The 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 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 , which also wipes out whatever that coordinate was already holding. Making the writing conditional takes another factor of the same kind, the input gate:
That leaves the question of what the rest of the network gets to see. If the hidden state were (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:
That's what the vanilla RNN couldn't do: its had two jobs at once, remembering and showing. The LSTM splits them: remembers, and shows.
Putting the four pieces together (three gates and a candidate), the full step looks like this, with for the same reason as in the RNN:
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 , and , and there's the price: the LSTM costs weights, four times as many as the vanilla RNN. What it doesn't cost is length: here too, 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 becomes visible:
Coordinate of the new cell depends on coordinate 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
a number, not a matrix. Chain it from the end back to a distant step , the way the previous lesson chained the backward recurrence, and what reaches the cell at along that path is
Set that beside the previous lesson's 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 mask biting at every step along this path (no 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: is both reachable and stable, because a saturated sigmoid sits at with almost zero slope, so a coordinate the network learns to keep open multiplies by as many times as needed. The RNN's 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
, and are computed from , and depends on . So the full derivative adds, to the above, the paths that run through the three gates and through the candidate, and every one of them crosses a 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 around , 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 decides how much the network remembers before it has learned anything. Look at the last line of each block.
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))
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 and each takes a different value in every coordinate, which is all the formula promised. Now compare the two blocks. With , the forget gate hovers around and about of makes it to ; with , it sits near and 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 to a positive value: it's cheaper for the network to learn to forget than to learn to remember. The first row of , on the other hand, is identical in both blocks: at the gate multiplies .
Try that same bias yourself. In the explorable below you set by hand and follow every coordinate over another six-step sequence, plus one extra row for the product the previous section derived, : how much of survives at each step that follows. The hidden state stays out of view again, as in the cell above, because what matters here is the additive path of .
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.
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)))
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, gets through: the matrix accounts for and the mask for the rest (another order of magnitude), the that the previous lesson said could only make things worse. The second line shows the LSTM coming out worse than the RNN: with the gate hovers around , and thirty-nine factors like that leave about 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 , survives, and at , 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 coordinates reading embeddings of . 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, ; only the activation changes: for the three gates, 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.
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
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
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
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.