Vanilla RNN: Forward & BPTT - Sequence Models
Implement Vanilla RNN: Forward & BPTT in Python with NumPy. hard exercise with examples, hints, and instant feedback. No setup or signup required.
Vanilla RNN: Forward & BPTT
What you'll build
Two functions: rnn_forward, which runs a vanilla (Elman) RNN over a batch of sequences, and rnn_backward, which computes every gradient by hand with backpropagation through time (BPTT).
Background
An RNN reads a sequence one step at a time and carries a hidden state h that summarizes everything seen so far, reusing the same weights at every step. Writing BPTT yourself shows exactly why plain RNNs suffer from vanishing and exploding gradients, a classic interview topic, and it is the same backward pass that autograd runs for you.
Forward
h_t = tanh(x_t @ W_xh + h_{t-1} @ W_hh + b) for t = 1..T, starting from h_0
Backward (BPTT)
Given dH = dL/dH_all of shape (N, T, H) (the loss may depend on every hidden state), walk the sequence backwards. The gradient reaching h_t has two parts: the direct one, dH[:, t], and the one flowing back from h_{t+1}:
dh_next = 0
for t = T..1:
dh = dH[:, t] + dh_next # direct + from the future
da = dh * (1 - h_t ** 2) # through tanh (tanh' = 1 - tanh^2)
dW_xh += x_t.T @ da # shared weights: ACCUMULATE over t
dW_hh += h_{t-1}.T @ da
db += da.sum(axis=0)
dX[:, t] = da @ W_xh.T
dh_next = da @ W_hh.T # flows into h_{t-1}
dh0 = dh_next
The repeated multiplication by W_hh.T and (1 - h**2) is why gradients vanish or explode over long sequences.
Inputs (batch first)
| Tensor | Shape | Meaning |
|---|---|---|
X | (N, T, D) | N sequences of T steps with D features |
h0 | (N, H) | initial hidden state |
W_xh | (D, H) | input-to-hidden weights, used as x @ W_xh |
W_hh | (H, H) | hidden-to-hidden weights, used as h @ W_hh |
b | (H,) | bias |
H_all | (N, T, H) | output of rnn_forward (input to rnn_backward) |
dH | (N, T, H) | upstream gradient dL/dH_all |
Layout note: this problem uses (in, out) weights and one bias. nn.RNN stores the transpose, weight_ih_l0 of shape (H, D), and has two biases (b_ih + b_hh).
Expected return format
rnn_forwardreturnsH_all, shape(N, T, H):H_all[:, t]is the state after reading stept.h_0is not included.rnn_backwardreturns a dictionary with these keys, each the same shape as the matching input:'dX':(N, T, D)'dh0':(N, H)'dW_xh':(D, H)'dW_hh':(H, H)'db':(H,)
Edge cases and conventions
T = 1works: one step, anddh0 = da @ W_hh.T.- At
t = 0the previous state ish0, not zeros. - Recompute nothing: the tanh derivative uses the stored outputs
H_all.
Constraints
NumPy only. Loops over time are expected (the recurrence is sequential); vectorise over the batch.
Follow-up
- Why does gradient clipping fix exploding but not vanishing gradients?
- What is truncated BPTT and what does it trade away?
Examples
Input: X = np.ones((1, 3, 1)), h0 = [[0.0]], W_xh = [[0.5]], W_hh = [[1.0]], b = [0.0] Output: [[[0.462117], [0.74522], [0.846938]]]
h1 = tanh(0.5), h2 = tanh(0.5 + h1), h3 = tanh(0.5 + h2): the state accumulates evidence.
Input: X: (2, 3, 4), h0: (2, 5), W_xh: (4, 5), W_hh: (5, 5); rnn_backward(...)
Output: {'dX': (2, 3, 4), 'dh0': (2, 5), 'dW_xh': (4, 5), 'dW_hh': (5, 5), 'db': (5,)}Every gradient has the shape of the tensor it differentiates (shapes shown).
Practice in Python
import numpy as np
def rnn_forward(X, h0, W_xh, W_hh, b):
"""
Run a vanilla RNN over a batch of sequences:
h_t = tanh(x_t @ W_xh + h_{t-1} @ W_hh + b).
Args:
X: Inputs (N, T, D), batch first
h0: Initial hidden state (N, H)
W_xh: Input-to-hidden weights (D, H)
W_hh: Hidden-to-hidden weights (H, H)
b: Bias (H,)
Returns:
H_all: Hidden states for every step (N, T, H), h0 not included
"""
# Your code here
pass
def rnn_backward(X, h0, W_xh, W_hh, H_all, dH):
"""
Backpropagation through time.
Args:
X, h0, W_xh, W_hh: Same as rnn_forward
H_all: Output of rnn_forward (N, T, H)
dH: Gradient of the loss w.r.t. H_all (N, T, H)
Returns:
dict with 'dX' (N, T, D), 'dh0' (N, H), 'dW_xh' (D, H),
'dW_hh' (H, H) and 'db' (H,)
"""
# Your code here
pass
The interactive editor loads with Python and NumPy. Run your implementation against 7 tests, or use the progressive hints.