1 The gating idea
Imagine you're listening to a long story so you can answer a question at the end. You don't try to memorise every single word β that's impossible. Instead you keep a little notepad: when something important happens ("the key is under the mat") you write it down, when something stops mattering you cross it out, and when someone asks you a question you read out only the relevant bits. A "gate" is just a tiny decision: let this through, or not? Today's networks learn to make those write / cross-out / read-out decisions all by themselves.
First, a quick recap of the problem
Recall from Session 12 that a plain RNN
(Recurrent Neural Network) processes a sequence one step at a time, carrying a
hidden state h_t from each step to the
next. At every step it does roughly:
# plain RNN update (from Session 12)
h_t = tanh(W_h Β· h_{t-1} + W_x Β· x_t + b)
The trouble is in that little h_{t-1} term. To learn from a clue
100 steps ago, the training signal (the gradient) has to travel back through 100 of those
multiplications. Each step multiplies by roughly the same weight matrix and a
tanh derivative that's less than 1. Multiply 100 numbers that are
each < 1 together and you get something microscopic β the vanishing
gradient problem. The opposite can also happen (numbers > 1 multiplied many times
blow up) β the exploding gradient problem.
A vanished gradient means the network literally cannot learn long-range dependencies. By the time the error signal travels back to step 1, it's so faint that the early weights barely change. The network forgets the start of the sentence β exactly when "remembering the start" is the whole point (e.g. matching a pronoun to a noun mentioned paragraphs earlier).
The insight: stop forcing it, let it learn
A plain RNN completely rewrites its memory at every single step β it has no
choice. The hidden state is squashed through a tanh and replaced.
That constant rewriting is exactly what destroys the gradient.
Don't hard-code when to remember. Give the network small, learnable gates β values between 0 and 1 produced by the network itself β that decide how much information to keep, throw away, or read out at each step. The network learns the right remembering policy from data, just like it learns everything else.
What exactly is a gate?
A gate is a vector of numbers between 0 and 1, produced by a
tiny neural layer with a sigmoid activation
(Ο, which always outputs 0β1). You then multiply it
element-wise (the β symbol) against some information:
- A gate value near 0 = "block this" (multiply by ~0 β information disappears).
- A gate value near 1 = "let it all through" (multiply by ~1 β information passes untouched).
- Values in between = "let some fraction through" β gates are soft, not on/off switches, which is what makes them trainable by gradient descent.
Gates use Ο because we want a clean 0-to-1 "how much" knob.
Actual content (the new candidate memory) usually uses tanh,
which outputs β1 to +1, so memory can move in either direction. Watch for this pairing β
it shows up in every gated cell below.
Suppose our memory is a single number c, currently
c = 5.0 (it means "the subject is plural"). A new word arrives.
The network computes a forget gate from the input and gets f = 0.9
(mostly keep), and an input gate i = 0.2 with new candidate
content g = β1.0. The updated memory is:
c_new = f β c + i β g
= 0.9 Γ 5.0 + 0.2 Γ (β1.0)
= 4.5 β 0.2
= 4.3
The memory was mostly preserved (because f was
high) with a small nudge from the new word. Nobody told it to keep 90% β
it learned that gate value because keeping plural-ness around was useful for the loss.
2 The LSTM cell anatomy
Picture a factory conveyor belt running straight through a room. Boxes (memories) ride along it. As the belt passes three little workers, each worker can act on it: the first worker can toss boxes off the belt (forget), the second can place new boxes on (add), and the third decides which boxes to show to the next room (output). The belt itself barely changes as it rolls along β that's the secret. An LSTM is exactly this: a memory belt plus three gates.
The LSTM (Long Short-Term Memory) cell was introduced by Hochreiter & Schmidhuber in 1997. Its key innovation is keeping two things flowing through time instead of one:
- The cell state
c_tβ the long-term memory "conveyor belt." It's edited only by gentle additions and removals. - The hidden state
h_tβ the short-term, "what I output right now" state (same role as in a plain RNN).
The three gates at a glance
| Gate | Symbol | Question it answers | Conveyor-belt job |
|---|---|---|---|
| Forget gate | f_t | What in the old memory should we erase? | Tosses boxes off the belt |
| Input gate | i_t | What new info should we write to memory? | Places new boxes on the belt |
| Output gate | o_t | What part of memory do we reveal right now? | Decides which boxes to show next room |
The data flow, as a diagram
The equations, one by one
At each timestep the LSTM first glues together the previous hidden state
h_{t-1} and the current input x_t, then runs
five small computations. Don't be scared β they're all the same shape: a weighted
sum followed by an activation. Let's read each line like a sentence.
# Ο = sigmoid (outputs 0..1, a "how much" knob) # β = element-wise multiply f_t = Ο( W_f Β· [h_{t-1}, x_t] + b_f ) # 1. forget gate i_t = Ο( W_i Β· [h_{t-1}, x_t] + b_i ) # 2. input gate g_t = tanh( W_g Β· [h_{t-1}, x_t] + b_g ) # 3. candidate memory c_t = f_t β c_{t-1} + i_t β g_t # 4. update the cell state o_t = Ο( W_o Β· [h_{t-1}, x_t] + b_o ) # 5. output gate h_t = o_t β tanh( c_t ) # 6. new hidden state
Line by line, in plain English:
- 1 β Forget gate
f_t: looks at the new input and old hidden state and outputs a number 0β1 per memory slot. 0 means "erase this slot," 1 means "keep it." - 2 β Input gate
i_t: decides how much of the brand-new candidate we'll actually write. - 3 β Candidate memory
g_t: the new information we could add, squashed to β1..+1 bytanhso it can push memory up or down. - 4 β The update (the heart of it):
c_t = f_t β c_{t-1} + i_t β g_t. Read it as: "keep the fractionf_tof the old belt, then add the fractioni_tof the new content." This is the famous additive update β remember it for Topic 3. - 5 β Output gate
o_t: decides which parts of the (now updated) memory to expose to the outside world this step. - 6 β Hidden state
h_t: the cell state passed throughtanhand filtered by the output gate. This is what leaves the cell and feeds the next layer / the prediction.
[h_{t-1}, x_t]
The square brackets just mean "stack the two vectors into one long vector," then one
weight matrix acts on the whole thing. In code you often see this as two separate matrices
(WΒ·h + UΒ·x) β mathematically identical, just split. Each gate has
its own learned weights, so the cell has 4 sets of weights total.
Sentence: "The dogs that chased the cat across the yard ... were tired." The verb must be "were" (plural) to match "dogs," even though many words intervene.
- At "dogs," the input gate opens (
i_t β 1) and writes "subject = plural" into a memory slot viag_t. - Through "that chased the cat across the yard," the forget gate stays near 1
for that slot (
f_t β 1), so plural-ness rides the conveyor belt untouched while other slots churn. - At the verb, the output gate opens for that slot, revealing "plural" so the network predicts "were," not "was."
No human wrote those gate values β they emerged from training to minimise prediction error.
c_t
(the conveyor belt) and the short-term hidden state h_t.
Three gates control it β forget (erase old memory), input
(write new memory via candidate g_t), and output
(reveal memory as h_t). The cell state is updated additively:
c_t = f_t β c_{t-1} + i_t β g_t.
3 Why LSTMs fix gradient flow
Imagine passing a whispered secret down a line of 100 kids. In a plain RNN, every kid re-says it in their own words β by kid 100 the message is total nonsense. In an LSTM there's a special rule: most kids are allowed to just pass the note along unchanged. A clear note can travel all the way down the line. That "pass it along unchanged" highway is the cell state, and it's why the message (the gradient) survives.
Recall what killed the plain RNN
In a plain RNN the hidden state is transformed every step:
h_t = tanh(W Β· h_{t-1} + ...). When we backpropagate, the gradient
of h_t with respect to h_{t-1} includes the
weight matrix W and a tanh' factor (always
< 1). Chain those across many steps and you're multiplying many small numbers β the
product shrinks toward zero. Gradient vanishes.
The additive path is the whole trick
Now look at the LSTM's cell-state update again, and ask: how does
c_t depend on c_{t-1}?
c_t = f_t β c_{t-1} + i_t β g_t
# the part of the gradient that travels straight back in time:
βc_t / βc_{t-1} = f_t # just the forget gate!
This is the magic. The connection from one cell state to the next is essentially
multiplication by the forget gate β and nothing else. There's no
weight matrix and no tanh squashing on this path. So when the
gradient flows back through time along the cell state, it gets multiplied by
f_t at each step.
If the network learns to keep a forget gate open
(f_t β 1) for a memory slot it cares about, then the gradient down
that path is multiplied by β1 over and over β so it neither vanishes nor
explodes. The cell state is a near-uninterrupted highway for gradients to travel
far back in time. This is sometimes called the constant error
carousel β error "rides the carousel" without decaying.
Plain RNN, with an effective per-step factor of 0.8:
gradient after 50 steps β 0.8^50 β 0.000014 # vanished
LSTM, with the network keeping the relevant forget gate near 1.0 (say 0.99):
gradient after 50 steps β 0.99^50 β 0.61 # very much alive
Same depth, wildly different survival β purely because the LSTM's path multiplies by a learnable gate that it can choose to hold open, instead of a fixed shrinking factor.
The deeper reason this works: c_t is built by adding to
the old state (+ i_t β g_t) rather than replacing it. Additive
updates have gradients that flow through unchanged β the very same principle behind
residual / skip connections in deep CNNs and Transformers (we'll
revisit those in Session 14). Gating + addition = the recurring recipe for
training very deep or very long networks.
LSTMs greatly reduce vanishing gradients but don't make them mathematically impossible β if the network learns small forget gates, memory still fades. And exploding gradients can still occur, which is why people pair LSTMs with gradient clipping (capping the gradient's size during training). LSTMs make long-range learning practical, not magic.
βc_t/βc_{t-1} = f_t) β no weight matrix, no tanh squashing. If the
network holds that gate open (β1), gradients survive across hundreds of steps: the
"constant error carousel." This additive memory path is the same idea as residual
connections, and it's why LSTMs can learn long-range dependencies that plain RNNs cannot.
4 GRUs β the leaner gated cell
The LSTM was a factory with three workers and two conveyor belts β powerful, but a lot of machinery. The GRU is a tidier shop: one belt and two workers. One worker decides "how much of the old memory to keep vs. replace" and the other decides "how much of the past to even look at when writing the new memory." Fewer parts, often just as good, and faster to run.
The GRU (Gated Recurrent Unit), introduced by Cho et al. in 2014, keeps the gating idea but simplifies it. The two big changes versus an LSTM:
- It has no separate cell state β the hidden state
h_tcarries both long- and short-term memory. - It uses two gates instead of three: a reset gate and an update gate.
The GRU equations
# Ο = sigmoid, β = element-wise multiply r_t = Ο( W_r Β· [h_{t-1}, x_t] + b_r ) # 1. reset gate z_t = Ο( W_z Β· [h_{t-1}, x_t] + b_z ) # 2. update gate h~_t = tanh( W_h Β· [r_t β h_{t-1}, x_t] + b_h ) # 3. candidate state h_t = (1 β z_t) β h_{t-1} + z_t β h~_t # 4. blend old & new
Reading it as a story:
- Reset gate
r_t: when computing the new candidate memory, how much of the past should we let influence it? Near 0 = "ignore the past, this is a fresh start" (great at sentence/clause boundaries). - Update gate
z_t: the star of the GRU. It does the job of the LSTM's forget and input gates at once. Line 4 says: keep fraction(1 β z_t)of the old state and take fractionz_tof the new candidate. They're tied together β what you don't keep, you replace. - Candidate
h~_t: the proposed new memory, computed using a reset-filtered view of the past.
Line 4 still has the precious additive structure: when the update gate
z_t β 0, we get h_t β h_{t-1} β memory
passes through nearly unchanged, so gradients flow back just like the LSTM's cell state. The
GRU keeps the gradient-friendly skip path while using fewer parameters.
LSTM vs GRU β side by side
| Aspect | LSTM | GRU |
|---|---|---|
| Gates | 3 (forget, input, output) | 2 (reset, update) |
| Memory states | 2 (cell c_t + hidden h_t) | 1 (hidden h_t only) |
| Parameters per cell | More (4 weight sets) | ~25% fewer (3 weight sets) |
| Speed / memory | Slower, heavier | Faster, lighter |
| Exposes full memory? | No β output gate filters it | Yes β whole state is the output |
| Best on | Very long / complex sequences; when you have lots of data | Smaller datasets, shorter sequences, when compute is tight |
- Long documents, big dataset, accuracy is king (e.g. document-level sentiment): start with an LSTM β its extra control sometimes squeezes out more performance on long dependencies.
- Modest dataset, want fast training, mobile/edge deployment (e.g. a small on-device text classifier): start with a GRU β fewer parameters means faster training and less overfitting risk.
In practice their accuracy is usually very close. The honest advice: try both and let your validation set decide. Neither is universally "better."
A GRU is a streamlined LSTM: it merges the cell and hidden states into one, and merges forget+input into a single update gate. You lose a little fine-grained control but gain speed and fewer parameters β and you keep the all-important additive gradient highway.
5 Bidirectional & stacked RNNs
Two simple upgrades. First: when you fill in a missing word in a sentence, you read both the words before and after the blank β a bidirectional RNN does the same by reading the sequence forward and backward. Second: one pair of eyes is good, but a team passing notes catches more β a stacked RNN piles several RNN layers on top of each other so deeper layers spot higher-level patterns.
Bidirectional RNNs β reading both directions
A normal RNN reads left-to-right, so its understanding of word t only uses words before it. But meaning often depends on what comes after. A bidirectional RNN (BiRNN) runs two independent RNNs: one forward (startβend) and one backward (endβstart). At each position it concatenates both hidden states, so every output knows about the entire sequence on both sides.
Tag the word "book":
- "I want to book a flight" β verb. You only know this from the word after ("a flight").
- "I read a book" β noun.
A forward-only RNN at "book" hasn't seen "a flight" yet. A BiRNN's backward pass has, so it disambiguates correctly.
BiRNNs need the whole sequence up front, so they're great for tasks like tagging or classification on complete texts. But for real-time generation or forecasting (predicting the next word/value as data streams in) you can't peek at the future β there is none yet. Use a forward-only model there.
Stacked (deep) RNNs β layers on layers
A stacked RNN (or deep RNN) feeds the sequence of hidden states from one RNN layer as the input sequence to the next RNN layer. Lower layers capture local patterns (lettersβwords), higher layers capture abstract ones (phrasesβmeaning) β the same "hierarchy of features" idea you saw with CNNs in earlier sessions.
2β4 layers is the common sweet spot. More layers add capacity but cost compute and can overfit; people add dropout between layers to regularise. You can absolutely combine ideas: a stacked bidirectional LSTM is a very common workhorse for sequence labelling.
Real PyTorch: it's basically free
The wonderful part: PyTorch's nn.LSTM gives you stacking and
bidirectionality with two arguments. You don't hand-write any of the gate equations from
Topic 2 β they're built in.
import torch import torch.nn as nn # A 2-layer BIDIRECTIONAL LSTM lstm = nn.LSTM( input_size=100, # size of each input vector (e.g. word embedding) hidden_size=128, # size of the hidden state per direction num_layers=2, # STACKED: 2 LSTM layers batch_first=True, # input shape is (batch, seq_len, features) bidirectional=True, # BIDIRECTIONAL: read both ways dropout=0.2, # dropout between stacked layers ) # Fake batch: 32 sequences, each 10 steps long, 100 features per step x = torch.randn(32, 10, 100) # output: per-step hidden states (both directions concatenated) # h_n, c_n: final hidden and cell states for each layer/direction output, (h_n, c_n) = lstm(x) print(output.shape) # (32, 10, 256) -> 128 * 2 directions print(h_n.shape) # (4, 32, 128) -> 2 layers * 2 directions
outputis(32, 10, 256): for all 32 sequences and all 10 timesteps, a 256-dim vector β that's 128 forward + 128 backward concatenated. Use this for per-token tasks (tagging).h_nis(4, 32, 128): the final hidden state, with the first axis =num_layers Γ num_directions = 2 Γ 2 = 4. Use the top layer's states for whole-sequence tasks (classification).- Swapping to a GRU is a one-word change:
nn.GRU(...)β and it returns justoutput, h_n(no cell state, as we learned in Topic 4).
Bidirectionality and stacking are orthogonal upgrades you can mix freely with
LSTM or GRU. In PyTorch they're just the bidirectional and
num_layers flags β the heavy lifting is done for you.
nn.LSTM / nn.GRU expose both via
the bidirectional and num_layers arguments.
β Putting it all together
This session was really about one elegant idea applied five ways. Here's the story that ties it all together:
Plain RNNs forget the distant past because they overwrite their memory every
step, making gradients vanish. The cure is gating:
learnable 0β1 knobs that decide how much information to keep, add, or reveal. The
LSTM bundles three gates (forget, input, output) around a long-term
cell-state "conveyor belt," updated additively
(c_t = f_t β c_{t-1} + i_t β g_t). That addition is exactly why
gradients survive β they ride the cell state, multiplied only by the forget gate (the
constant error carousel). The GRU achieves the same with
fewer parts: two gates, one state. And whichever cell you choose, you can make it
bidirectional (read both ways) and stacked (deep) β both
one-line flags in PyTorch. Next session we use these recurrent encoders to build
Seq2Seq models and meet attention, which eventually
makes the recurrence itself optional.
Quick self-check
Why does a plain RNN's gradient vanish but an LSTM's cell-state gradient doesn't?
A plain RNN multiplies the gradient by a weight matrix and a tanh derivative
(<1) at every step, so it shrinks. The LSTM's cell-state path multiplies only by the
forget gate (βc_t/βc_{t-1} = f_t); if the network holds that gate
near 1, gradients pass through nearly unchanged.
What do the three LSTM gates do?
Forget gate erases parts of the old cell state; input gate decides how much
new candidate content to write; output gate decides which parts of the cell state to expose
as the hidden state h_t.
How is a GRU different from an LSTM?
A GRU has only one state (the hidden state, no separate cell state) and two gates (reset and update). Its update gate does the job of the LSTM's forget+input gates together. It's lighter and faster with usually comparable accuracy.
When should you NOT use a bidirectional RNN?
When you can't see the future β real-time generation or forecasting, where you predict the next item as data streams in. BiRNNs need the entire sequence up front.
In nn.LSTM, you set hidden_size=128, num_layers=2, bidirectional=True. What's the last-dim size of output?
256 β that's hidden_size Γ num_directions = 128 Γ 2,
because the forward and backward hidden states are concatenated at each timestep.
Why is the additive update c_t = f_t β c_{t-1} + ... conceptually similar to a residual connection?
Both add to an existing signal rather than fully replacing it, so the gradient has an unobstructed path to flow backward. This shared principle is what lets both LSTMs and very deep residual networks train across long distances.
π References & Further Reading
Class material
- Your course handout for this session: "SST Deep Learning handout (Session 13)" β the primary reference for this class. (course page)
Papers, docs & deep dives
- Hochreiter & Schmidhuber, "Long Short-Term Memory" (1997) β the original paper that introduced the LSTM and the "constant error carousel" argument behind Topic 3.
- Christopher Olah, "Understanding LSTM Networks" β the legendary, diagram-rich blog post; the best companion to our cell-anatomy section.
- Cho et al., "Learning Phrase Representations using RNN EncoderβDecoder" (2014) β the paper that introduced the GRU (Topic 4).
- PyTorch
nn.LSTMdocumentation β official reference for the arguments and tensor shapes used in Topic 5. - Chung et al., "Empirical Evaluation of Gated Recurrent Neural Networks" (2014) β head-to-head comparison of LSTM vs GRU, backing the "benchmark both" advice.