GRU: the simplified version
26 min read
So far this block has moved forward by asking what each network was missing: the vanilla recurrent neural network (RNN) lacked a memory that could survive distance, and the previous lesson's long short-term memory (LSTM), on gated memory, supplied one. But it came at a cost: four times the weights of a vanilla RNN, and a second vector hauled through every step. And the lesson itself ended by doubting that every one of its gates was needed. Once an architecture works, the question is no longer what it's missing, but what it could do without.
Real simplification doesn't mean removing pieces until something breaks; it means removing them without losing what they paid for. And what the LSTM paid for (the one thing that justified all that machinery) was the return path, the route along which the gradient comes back intact across many steps. Every cut has to be checked against it: if merging gates or dropping states loses that path, the simplification has thrown away exactly what mattered. The gated recurrent unit (GRU) is the cut that passes the test, which is why it became the standard alternative to the LSTM in practice: a quarter fewer weights, one state, and (almost always, though not always) the same result.
The cut comes down to two ideas. The first fuses the forget gate and the input gate: instead of deciding separately how much to keep and how much to write, the GRU uses a single control (the update gate) that says how much to replace, and whatever it doesn't replace, it keeps. The state stops being a sum that can grow without limit and becomes a mix, a point partway between what was there and what's being proposed; and a mix of bounded things stays bounded, so the output gate that trimmed the LSTM's state before showing it is no longer needed.
The second idea is a small new gate, the reset gate, which leaves the memory alone and acts on the candidate instead: it lets the candidate ignore the previous state when what needs writing now carries nothing over from before (at the start of a new sentence, say). With those two gates, and a single state that serves as both memory and output, the second vector disappears: there's no longer a cell state hidden behind the one on display.
Before the formulas, take one more look at the LSTM the GRU simplifies. Step through it and watch the first two rows (the forget gate and the input gate) move independently: those are the two the GRU is about to tie into one.
Keeping and writing with a single gate
Start with the line that changes everything. The LSTM's cell added two terms with independent gates, ; the GRU works directly on the hidden state (there's no second vector) and ties the two gates into one:
where is the update gate and is the candidate, the state the step proposes. Coordinate by coordinate, it's a weighted average of old and new: leaves equal to (it keeps), and replaces it entirely with the candidate (it writes). There's no way to do both at once, and that's precisely the freedom the LSTM had and the GRU gives up: there, and could both open and accumulate. In exchange, an average of bounded numbers can't overflow, which is why the GRU shows its state as it is, without the output gate or the that trimmed the LSTM's state before letting it out.
The update gate is computed like every gate in the previous lesson, as a sigmoid layer over the token and the previous state:
with , and : the same shapes as the recurrence in the lesson on the vanilla RNN.
That leaves the candidate, and this is where the second gate comes in. In the LSTM, the candidate always read the whole previous state; the GRU puts a filter in front of it, the reset gate , which decides how much of that state the candidate gets to see:
That is the only thing that sets this candidate apart from a vanilla RNN step: with the candidate is exactly the recurrence from the lesson on the vanilla RNN, and with it's computed from the token alone, as if the sequence started there. Keep the two gates apart: decides whether the state gets updated, while decides what the candidate looks at when making its proposal. One governs the memory, the other the candidate.
Here are the three pieces together, with for the same reason as in the RNN and the LSTM:
Three pieces (two gates and a candidate), each with exactly the shape of the recurrence in the lesson on the vanilla RNN: six matrices and three biases, weights. That's three quarters of the LSTM's four groups; and as with the LSTM, appears in none of the shapes, so the GRU reads any length with a single set of weights.
Why the return path survives the cut
The whole lesson hangs on one check: that the return path (the one the LSTM paid for, and whose necessity the lesson on the vanishing gradient explained) is still standing with one state instead of two. Look at the last equation coordinate by coordinate, which is where the meaning of the becomes visible:
Along this path, coordinate of the new state depends on coordinate of the old one and on no other: there's no matrix to mix them. Differentiating along the direct path while holding the gates and the candidate still (I'll come back to this),
a number, not a matrix. Chain it backwards to a distant step , the way the lesson on the vanishing gradient chained the recurrence, and what arrives along the direct path is a product of those numbers:
It's the LSTM's letter for letter, with in place of : the closed update gate () plays the role of the open forget gate. And the same thing holds as before ( is reachable and stable, because a saturated sigmoid stays flat at its extreme), so a coordinate the network learns not to update multiplies by for as many steps as needed. Compared with the of the lesson on the vanishing gradient, the base is no longer a property of one matrix shared by everything; it's a number per coordinate and per step that the network computes.
See what "holding the gates still" left out
, and are computed from , so the full derivative adds, to the direct path's , the paths that run through the two gates and through the candidate. Every one of them crosses a and an activation mask, and they vanish the same way as the paths in the lesson on the vanishing gradient.
Their vanishing breaks nothing, and the reasoning is the same as for the LSTM. Those paths adjust how the decision is made at each step, and a decision at this step relies on what's nearby; the gradient that has to arrive from far away travels the direct path, and that path crosses no matrix. The GRU doesn't eliminate the product from the lesson on the vanishing gradient: it moves it off the long path and leaves it on the short ones.
Two concessions close the account, the same two as for the LSTM. If the network learns around , the product sinks as it did in the RNN, except that now it's because the network chose to, not because the architecture forces it. And explosion is still possible along the paths in the box above, so the GRU is trained with gradient clipping like everything before it. The additive path doesn't hand out a guarantee; it puts within reach an option that was out of reach before.
The GRU, step by step, in NumPy
One cell. It transcribes the four-equation block and runs it on a short sequence with two different update-gate biases, to see how much of reaches along the direct path.
Once again the weights come from a generator with a fixed seed and are untrained, so the gates don't mean anything yet. What I did set by hand is the bias : remember that keeping means , so a negative bias is what nudges the network towards remembering (the mirror image of the LSTM's positive ). 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(update_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 [("z", update_bias), ("r", 0.0), ("h", 0.0)]}
def step(P, h, x):
def pre(k, state):
Wx, Wh, b = P[k]
return Wx @ x + Wh @ state + b
z = sigmoid(pre("z", h))
r = sigmoid(pre("r", h))
cand = np.tanh(pre("h", r * h)) # the candidate reads r (*) h_{t-1}
h = (1 - z) * h + z * cand # the additive path, tied
return h, z, r
for bias in [0.0, -3.0]:
P = weights(bias)
h, kept = np.zeros(d_h), []
print("b_z = %.0f" % bias)
print(" t z_t r_t h_t")
for t, x in enumerate(X, 1):
h, z, r = step(P, h, x)
kept.append(1 - z)
print(" %d %s %s %s" % (t, np.round(z, 2), np.round(r, 2), np.round(h, 2)))
print(" what survives of h_1 in h_6:", np.round(np.prod(kept[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.
Both gates live in , and each coordinate gets its own value (where two print the same, they differ beyond the second decimal place). Compare the two blocks by their last lines. With , the update gate hovers around , half the state is replaced at every step, and about of reaches ; with , it hovers around , almost nothing gets updated, and survives, with the same weights and the same sequence. Hence the habit of initialising to a negative value, for the same reason as a positive in the LSTM: it's cheaper for the network to learn to write than to learn to remember. And notice the price of the shortcut: with the state barely moves ( stays in the hundredths), because keeping and writing are the same control, and asking it to keep means asking it to barely write.
Test your intuition
Four questions: what the update gate ties together and what it gives up, what the reset gate does without touching the memory, what the architecture costs, and what the GRU takes from the LSTM and what it leaves behind.
What does the GRU gain, and what does it lose, by replacing the LSTM's forget and input gates with a single update gate ?
The reset gate multiplies the previous state inside the candidate: . With , what happens?
Take a GRU with a state of coordinates reading embeddings of , the same dimensions as the previous lesson's LSTM. How many weights do its six matrices and three biases add up to?
A margin of ±0 is accepted.
Tick everything that is true of the GRU compared with the LSTM.
Select every correct option. This is graded all-or-nothing: there is no partial credit.
And then the full step, written by you: the recurrence from the vanilla RNN wrapped in two gates.
Write one GRU step: gru_step(h, x, P) takes the previous state, the current token's
vector, and a dictionary P holding the weights of the three pieces (P["z"], P["r"]
and P["h"], each a triple (Wx, Wh, b)). It returns h_new.
The two gates are sigmoids over the token and the previous state; the candidate is a that reads instead of the bare , and the new state is the mix . Compute both gates from the incoming state, not the one you're about to return. 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.
With the GRU and the LSTM, the block now has two ways of giving a network memory, and both rest on the same discovery: a path along which the state moves forward by adding, without crossing a matrix, is the only thing that lets the gradient come back from far away. But knowing how to build a cell isn't the same as having built something with it. In this entire block, no network has yet read a text from start to finish and produced language of its own: we've derived the machinery and never switched it on.
Switching it on is the job of the next lesson, a project that builds a character-level language model. It combines the vanilla RNN and backpropagation through time (BPTT) in a network that trains on a few thousand characters and then generates new text, character by character. The result will be bad (and I won't sugar-coat it when it is), it will run in your browser in a few seconds, and it will be the first time in the course that something writes.
Further reading2 sources · 2 papers
Where this lesson comes from, and where to go next. None of it is needed to carry on with the course.
- Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation
Its §2.3 introduces the reset and update gates you build here. Watch the sign: its z keeps and yours writes, so where you write z, it writes 1-z. The name "GRU" doesn't appear yet, and most of the paper is about translation.
- Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling
It settles on the name "GRU" and this lesson's convention, with a z that writes. Comparing GRU and LSTM on music and speech, it backs your "almost always, not always": roughly even, with no winner named. No text tasks.