Recurrent Neural Networks
Every network so far has treated each input as an isolated snapshot. Sequences — sentences, time series, audio — need a network with memory of what came before.
Every architecture up to this point — perceptron, feedforward net, CNN — assumes a fixed-size input processed all at once, with no notion of order. That assumption breaks for text, audio, sensor streams, or anything where length varies and sequence matters. This lecture is the first architecture built around the idea that the order of the input is itself information. (One honest gap: for text, each input \(x_t\) should really be a word embedding — how words become vectors is Lecture 19's job, deliberately deferred until after we've seen why sequence models need it. Until then, treat each \(x_t\) as a generic numeric vector, exactly as you would for any other time series, like daily stock prices.)
- Explain why a feedforward network cannot solve tasks where the correct output depends on earlier inputs, not just the current one.
- Write down and interpret the RNN recurrence equations, including which parameters are shared across timesteps.
- "Unroll" a recurrent network through time into an equivalent deep feedforward computation graph.
- Compute a small RNN's hidden states by hand across several timesteps.
- State why weight sharing keeps the parameter count independent of sequence length.
- Name Backpropagation Through Time (BPTT) as the training algorithm and preview the failure mode it exposes (Lecture 16).
1. Why Feedforward Isn't Enough
Every network we have built up to now — Lectures 4 through 14 — is a pure function of its current input. Feed it an image, or a fixed-length feature vector, and it produces an output using only what's in front of it right now. That's exactly right for classifying a single image or a single tabular row. It breaks down the moment order and history matter: time-series forecasting, speech, and — the running example for this module — text.
The instructor's framing is direct: RNNs exist for time-series / sequence analysis — for tasks where "we need to make a prediction based on previous data, not only the current data." A feedforward network has no mechanism to carry information from one input to the next; each forward pass starts from a blank slate.
Consider text generation on the sentence "She spoke to her husband". Suppose the model must predict the second occurrence of the word "her". The immediately preceding word is just "to" — on its own, "to ___" could be completed a thousand ways. To know the missing word should be "her" (referring back to the subject), the model needs context reaching further back: "she, spoke". A network that only ever sees the current token, with no memory of the tokens before it, cannot make this prediction reliably.
This is the gap a Recurrent Neural Network (RNN) closes: instead of only mapping input → output, it maintains a running internal state — the hidden state — that is updated at every timestep and carried forward, so past inputs continue to influence future predictions.
2. The Recurrent Equations
An RNN processes a sequence of inputs \(x_1, x_2, \ldots, x_T\) one timestep at a time. At each step it combines the current input with the previous hidden state to produce a new hidden state, then optionally produces an output from that hidden state. Using the instructor's exact notation, with weight matrices \(W_h\) (hidden-to-hidden) and \(W_x\) (input-to-hidden), and an initial hidden state \(h_0\) (usually zero):
$$h_1=\tanh(W_h h_0+W_x x_1) \qquad y_1=W_y h_1$$ $$h_2=\tanh(W_h h_1+W_x x_2) \qquad y_2=W_y h_2$$ $$\ldots \qquad h_t=\tanh(W_h h_{t-1}+W_x x_t) \qquad y_t=W_y h_t$$Look closely: \(W_h\), \(W_x\) and \(W_y\) carry no timestep subscript. The exact same three matrices are reused — "shared" — at every single timestep, no matter how long the sequence is. This is what makes an RNN a genuinely recurrent (self-referential) computation rather than just a very deep, differently-parameterized feedforward stack.
The hidden state \(h_t\) is the network's memory: it is a compressed summary of everything the network has seen from \(x_1\) up through \(x_t\). Because \(h_t\) feeds into the computation of \(h_{t+1}\), information from early timesteps can (in principle) still influence outputs many steps later — exactly the capability the "She spoke to her husband" example demanded.
3. Unrolling Through Time
The recurrence \(h_t=\tanh(W_h h_{t-1}+W_x x_t)\) is easiest to reason about — and to train — if we "unroll" it: draw one copy of the computation per timestep, laid out left to right, with the shared weights redrawn at every copy. The result is an equivalent deep feedforward network with \(T\) layers, one layer per timestep, where every layer happens to use identical weights.
Let's compute this by hand for a simple scalar RNN, so "unrolling" stops being an abstract picture. Take \(W_h=0.5\), \(W_x=0.8\), \(h_0=0\), and a short input sequence \(x_1=1.0,\ x_2=0.5,\ x_3=-0.3,\ x_4=0.2\) (the fourth value is added here purely to extend the pattern one more step). Every timestep applies the exact same rule \(h_t=\tanh(W_h h_{t-1}+W_x x_t)\) — only the two inputs to \(\tanh\) change from step to step, never the weights. Here is the full arithmetic, one timestep at a time:
Plug in \(h_0=0\) and \(x_1=1.0\), multiply, add, then apply \(\tanh\):
$$h_1=\tanh(0.5\times0 + 0.8\times1.0)=\tanh(0+0.8)=\tanh(0.8)$$
Using \(\tanh(z)=\dfrac{e^{z}-e^{-z}}{e^{z}+e^{-z}}\): \(e^{0.8}\approx2.2255\), \(e^{-0.8}\approx0.4493\), so \(\tanh(0.8)=\dfrac{2.2255-0.4493}{2.2255+0.4493}=\dfrac{1.7762}{2.6748}\approx\mathbf{0.6640}\).
Now \(h_1=0.6640\) (just computed) takes the place of \(h_{t-1}\), paired with \(x_2=0.5\):
$$h_2=\tanh(0.5\times0.6640 + 0.8\times0.5)=\tanh(0.3320+0.4000)=\tanh(0.7320)\approx\mathbf{0.6244}$$
Same two weights (0.5 and 0.8) as step 1 — only the numbers being multiplied changed, because \(h_1\) and \(x_2\) are new.
Carry \(h_2=0.6244\) forward, and this time \(x_3=-0.3\) is negative:
$$h_3=\tanh(0.5\times0.6244 + 0.8\times(-0.3))=\tanh(0.3122-0.2400)=\tanh(0.0722)\approx\mathbf{0.0721}$$
Notice how the negative input \(x_3\) pulls the pre-activation sum sharply back down toward zero — the hidden state is genuinely reacting to each new input, not just drifting.
Finally, \(h_3=0.0721\) combines with \(x_4=0.2\):
$$h_4=\tanh(0.5\times0.0721 + 0.8\times0.2)=\tanh(0.0361+0.1600)=\tanh(0.1961)\approx\mathbf{0.1937}$$
Four timesteps, four different \(\tanh(\cdot)\) evaluations — but the same pair of numbers, 0.5 and 0.8, doing the multiplying every single time. That repetition is exactly what "weight sharing" means in concrete arithmetic terms.
You can replay the same four steps interactively below — useful for testing yourself before moving on:
Every one of the four steps used the identical formula \(h_t=\tanh(W_h h_{t-1}+W_x x_t)\) with the identical numbers 0.5 and 0.8 for \(W_h,W_x\) — only \(h_{t-1}\) and \(x_t\) changed. This is weight sharing in action, and it's the entire reason an RNN unrolled to 4 steps and an RNN unrolled to 400 steps are described by the same two numbers.
4. Why Weight Sharing Is Essential
Imagine instead that each timestep had its own, independently-learned weight matrices: \(W_h^{(1)}, W_h^{(2)}, \ldots\) This design has two fatal problems for sequence data:
- Parameter count would grow with sequence length. A sentence of 5 words and a sentence of 500 words would need entirely different-sized models — impossible, since a language model must handle sentences of any length with one fixed set of parameters.
- Nothing learned at position 3 would transfer to position 30. The whole point of processing "she spoke to her husband" is that the same underlying linguistic rule — "carry subject information forward until it's needed" — should apply no matter where in the sentence it's needed. Tying the weights together forces the network to learn one general-purpose update rule instead of memorizing per-position quirks.
Weight sharing is what lets a single learned function — the RNN cell — be applied uniformly across a sequence of arbitrary length. This is the same intuition behind sharing a convolutional kernel across every spatial location in a CNN (Lecture 13): one small, reusable computation, applied repeatedly.
5. Training: Backpropagation Through Time
How is an RNN trained? Once unrolled, an RNN is a deep feedforward computation graph — so it is trained with exactly the algorithm from Lecture 8: backpropagation. Applied to the unrolled graph, this variant has its own name: Backpropagation Through Time (BPTT).
Run the network forward across all \(T\) timesteps, compute the loss, then apply the chain rule backward through the unrolled graph exactly as in Lecture 8 — except that because \(W_h\), \(W_x\), \(W_y\) are the same matrices at every timestep, the gradient with respect to each of them is accumulated (summed) across every timestep it appears in before a single weight update is applied.
That accumulation step sounds harmless, but it is where a very specific and important failure mode originates: summing many gradient contributions, each one itself a product of many small terms from the chain rule, over a long sequence. That is precisely the subject of Lecture 16 — The Vanishing/Exploding Gradient Problem, where we'll see exactly why long sequences make this accumulation numerically treacherous.
6. Limitations to Keep in Mind
- Sequential computation. \(h_t\) cannot be computed until \(h_{t-1}\) exists, so RNNs process a sequence strictly one step at a time — unlike CNNs or fully-connected layers, this cannot be parallelized across the time dimension.
- A fixed-size memory bottleneck. No matter how long the input sequence, all of its history must be compressed into one hidden vector \(h_t\) of fixed dimension.
- Long-range dependencies are hard to learn — the BPTT gradient accumulation mentioned above tends to make information from many timesteps ago vanish before it can influence the loss. Lecture 16 makes this precise; Lecture 17's LSTM is the architectural fix.
7. Summary
- RNNs add memory to neural networks via a hidden state \(h_t=\tanh(W_h h_{t-1}+W_x x_t)\), updated once per timestep.
- \(W_h\), \(W_x\), \(W_y\) are shared across every timestep — this keeps the parameter count fixed regardless of sequence length and forces one general update rule.
- "Unrolling" turns the recurrence into an equivalent deep feedforward graph with \(T\) weight-tied layers — a useful mental (and computational) picture.
- Training uses Backpropagation Through Time (BPTT): Lecture 8's algorithm, applied to the unrolled graph, with gradients for the shared weights accumulated across all timesteps.
- That accumulation is exactly what causes the vanishing/exploding gradient problem — Lecture 16.
8. Code: A Scalar RNN Forward Pass
The NumPy script below reproduces the unrolled hand-computation above exactly — run it and confirm \(h_1=0.6640,\ h_2=0.6244,\ h_3=0.0721\) match.
import numpy as np
# Scalar RNN forward pass reproducing the lecture's worked example.
# h_t = tanh(Wh*h_{t-1} + Wx*x_t) y_t = Wy*h_t
# Wh, Wx, Wy are SHARED across every timestep -- that's the whole point.
Wh, Wx, Wy = 0.5, 0.8, 1.0
h0 = 0.0
xs = [1.0, 0.5, -0.3, 0.2] # x1..x4
h = h0
hidden_states = []
for t, x in enumerate(xs, start=1):
h = np.tanh(Wh * h + Wx * x)
hidden_states.append(h)
print(f"h{t} = tanh({Wh}*h{t-1} + {Wx}*{x}) = {h:.4f}")
ys = [Wy * h for h in hidden_states]
print("Outputs y1..y4:", [f"{y:.4f}" for y in ys])
# ---- weight sharing: parameter count is independent of sequence length ----
n_params_rnn = 3 # Wh, Wx, Wy -- fixed, regardless of T
n_params_naive_ff = 3 * len(xs) # if every timestep had its OWN weights
print(f"RNN params: {n_params_rnn} (shared across all {len(xs)} steps)")
print(f"Naive per-timestep FF params: {n_params_naive_ff} (grows with sequence length)")
⬇ Download lecture-15-rnn-forward.py More resources for this lecture →