Skip to content

← Recurrent Networks step 6 of 6

Hard End-to-End

One RNN Language-Model Training Step

Train a small RNN to predict the next token in a sequence. Implement one forward pass, manual backpropagation through time (BPTT), and one simultaneous SGD update. Derive the gradients yourself; do not use autograd or a built-in RNN module in your solution.

Inputs and return value

Implement train_rnn_lm_step(tokens, w_emb, w_h, w_out, h0, lr).

  • tokens has shape (T,), with T at least 2. Entries are integer-valued token IDs in [0, V), supplied as numbers; cast them to integers for indexing.
  • w_emb has shape (V, d_emb).
  • w_h has shape (d_emb + d_h, d_h).
  • w_out has shape (d_h, V).
  • h0 has shape (d_h,).
  • lr is a nonnegative learning rate.

All dimensions are positive. The weights and initial state are floating-point arrays. This is one sequence, with no batch axis, biases or dropout. Work on local parameter copies and leave the input arrays unchanged.

Compute updates for w_emb, w_h and w_out, treating h0 as fixed. Return only the updated w_out flattened in row-major order, with shape (d_h * V,). This output format checks the output-projection update; it does not independently verify the embedding or recurrent-weight updates. Implement the full backward pass described below as part of the exercise.

Align inputs and targets

There are n_steps = T - 1 predictions. Input tokens are tokens[:-1] and targets are tokens[1:]. For [2, 0, 2, 1], train on the pairs 2 -> 0, 0 -> 2, and 2 -> 1. The last token is a target, not another input step.

Starting from h0, compute and save each step's input embedding, previous hidden state, updated hidden state and probabilities:

embedding = w_emb[tokens[t]]
joined = concatenate([embedding, previous_h])
h = tanh(joined @ w_h)
logits = h @ w_out
probs = softmax(logits)

Use stable softmax over the V vocabulary entries. The objective is mean cross-entropy over all n_steps, not a sum. You do not return the loss; if you calculate it for debugging, use stable log-softmax rather than taking the log of a probability that may have underflowed.

Backpropagate from last step to first

Initialize all parameter gradients and dh_next to zero. At each step t, use saved forward values and the original, pre-update weights:

dlogits = (probs[t] - one_hot(tokens[t+1])) / n_steps
dw_out += outer(h[t], dlogits)
dh = w_out @ dlogits + dh_next
dpre = dh * (1 - h[t]**2)
dw_h += outer(joined[t], dpre)
djoined = w_h @ dpre
dw_emb[tokens[t]] += djoined[:d_emb]
dh_next = djoined[d_emb:]

Here h[t] means the updated hidden state after input t. For t=0, joined[t] contains h0; later steps contain the preceding updated state. dh_next is the contribution arriving through the recurrence from later predictions. Add it to the current output contribution before differentiating tanh.

Accumulate gradients rather than overwriting them. Repeated input tokens contribute multiple times to the same embedding row. The division by n_steps already accounts for the mean loss; do not divide the accumulated gradients again. The recurrence uses matrix products as well as tanh derivatives, so its gradients can shrink or grow rather than always vanishing.

Apply one update

Only after the full backward pass, compute new_parameter = parameter - lr * gradient for all three weight arrays. Updating parameters inside the time loop would change the gradients being computed. Return the flattened new w_out.

With lr=0, the result must equal the original flattened w_out. Allocate helper arrays with the appropriate input dtype and device in PyTorch.