EE 641 - Unit 4A
Dr. Brandon Franzke
Fall 2026
nn.RNNTranslating a Sentence · Reading the Source · Writing the Target · Choosing the Words · Training the Pair
[BPTT] P. J. Werbos, “Backpropagation through time: What it does and how to do it,” Proceedings of the IEEE, vol. 78, no. 10, pp. 1550–1560, 1990.
[RNN] I. Goodfellow, Y. Bengio, and A. Courville, “Deep Learning,” MIT Press, 2016, Chapter 10: Sequence Modeling: Recurrent and Recursive Nets.
[Gradients] Y. Bengio, P. Simard, and P. Frasconi, “Learning long-term dependencies with gradient descent is difficult,” IEEE Transactions on Neural Networks, vol. 5, no. 2, pp. 157–166, 1994.
[Clipping] R. Pascanu, T. Mikolov, and Y. Bengio, “On the difficulty of training recurrent neural networks,” in Proceedings of the 30th International Conference on Machine Learning, 2013, pp. 1310–1318.
[LSTM] S. Hochreiter and J. Schmidhuber, “Long short-term memory,” Neural Computation, vol. 9, no. 8, pp. 1735–1780, 1997.
[GRU] K. Cho, B. van Merriënboer, C. Gulcehre, D. Bahdanau, F. Bougares, H. Schwenk, and Y. Bengio, “Learning phrase representations using RNN encoder–decoder for statistical machine translation,” in Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing, 2014, pp. 1724–1734.
[Text] A. Karpathy, J. Johnson, and L. Fei-Fei, “Visualizing and understanding recurrent networks,” in International Conference on Learning Representations, Workshop Track, 2016.
One vector per step
Sources
| Source | Element \(\mathbf{x}_t\) | \(d\) | Rate |
|---|---|---|---|
| Text | one token, as an embedding vector | 100-1000 | one per word or subword |
| Audio | one frame of a mel spectrogram | 40-80 | 100 per second |
| Sensors | one reading of every channel | 3-50 | 50-1000 per second |
Lengths in three kinds of data
Requirements on the model
Examples
Relative position
Test
Language
Signals
Required of a model
First layer
Reshaping a sequence into \(D\) numbers
Parameter count
Separate weights per position
Length fixed at \(T_{\max}\)
Sum or mean over \(t\)
Permutation invariant, by construction
Task dependence
Across an image
Along time
Properties
Receptive field
Set by the architecture
Benchmarks
What a window lacks
Keeping a value instead
Cost
Two familiar update rules
Same loop, different rule
Recurrence
From one number to a vector
Update rules
| Memory | \(f(\mathbf{h}_{t-1}, \mathbf{x}_t)\) | What survives |
|---|---|---|
| Running sum | \(h_{t-1} + x_t\) | the total |
| Running maximum | \(\max(h_{t-1}, x_t)\) | the largest value |
| Recurrent network | a network with weights fitted to a task | what the task’s loss rewards |
Parts
Feedback system
Forgetting is forced
Forgetting is chosen at every step
Two tasks
Two questions
What training needs
Why the loop is opened
Depth set by the data
Compared with a feedforward network
Training
Recursion
Impulse response
Transfer function
Three regimes
Memory horizon
DC gain
Closed form
Modes
Spectral radius
Complex pair
Three \(2 \times 2\) cases
FIR: a window with fixed taps
IIR: feedback through a state
| FIR (window) | IIR (feedback) | |
|---|---|---|
| Reach | exactly \(k\) steps | unbounded, decaying as \(\alpha^t\) |
| Cost per step | \(k\) multiplications | one for the state, \(H^2\) for a vector state |
| Stability | any taps | $ |
| Sequence model | convolution over time | recurrent unit |
Setup
LMS for FIR taps
Same procedure as training a network
Same setup, one pole to adapt
Gradient as a recursion
Two consequences
Carried forward
Superposition
What no choice of \(\alpha\), \(\beta\) gives
The recurrent unit’s \(f\) is nonlinear for this reason.
Recurrence and readout
Against the linear recursion \(\mathbf{A}\mathbf{h}_{t-1} + \mathbf{B}\mathbf{x}_t\)
\(\tanh\) in the loop
| Object | Shape | Role |
|---|---|---|
| \(\mathbf{x}_t\) | \(\mathbb{R}^d\) | input at step \(t\) |
| \(\mathbf{h}_t\) | \(\mathbb{R}^H\) | state, the only carrier of the past |
| \(\mathbf{y}_t\) | \(\mathbb{R}^K\) | output at step \(t\) |
| \(\mathbf{W}_{xh}\) | \(\mathbb{R}^{H \times d}\) | projects the input into the state space |
| \(\mathbf{W}_{hh}\) | \(\mathbb{R}^{H \times H}\) | feedback: mixes the state with itself, once per step |
| \(\mathbf{b}\) | \(\mathbb{R}^H\) | sets where on \(\tanh\) the unit operates |
| \(\mathbf{W}_{hy}\) | \(\mathbb{R}^{K \times H}\) | reads the state out, one row per output |
Two of the three are outside the loop
One recurrence, \(d = 300\), \(H = 512\), two tasks
| Task | \(K\) | Recurrence \(H^2 + Hd\) | Readout \(KH\) | Total | Readout share |
|---|---|---|---|---|---|
| Tagging | \(50\) | \(415{,}744\) | \(25{,}600\) | \(441{,}906\) | \(5.8\%\) |
| Word prediction | \(10{,}000\) | \(415{,}744\) | \(5{,}120{,}000\) | \(5{,}546{,}256\) | \(92.3\%\) |
Memory is set by \(H\) alone
Readout is set by the task
Cost of one step
Parallel axes
What the backward pass reads
Memory, float32, batch \(B\)
| Tensor | Bytes |
|---|---|
| states | \(B \cdot T \cdot H \cdot 4\) |
| inputs | \(B \cdot T \cdot d \cdot 4\) |
| outputs | \(B \cdot T \cdot K \cdot 4\) |
Growth with \(T\)
def rnn_forward(x, h0, W_xh, W_hh, b, W_hy): # x: (B, T, d) h0: (B, H) B, T, d = x.shape h = h0 states, outputs = [], [] for t in range(T): z = x[:, t, :] @ W_xh.T + h @ W_hh.T + b # (B, H) h = torch.tanh(z) # (B, H) y = h @ W_hy.T # (B, K) states.append(h) outputs.append(y) return torch.stack(outputs, 1), torch.stack(states, 1) # outputs: (B, T, K) states: (B, T, H)
Line by line
x[:, t, :]: the batch’s step \(t\), one \(B \times d\) slice of the \((B, T, d)\) tensor above. Rows are examples and never interacth overwritten each step: the loop itself needs \(O(BH)\) memorystates kept for the backward passnn.RNN Takes \((B, T, d)\) and Returns Every Staternn = nn.RNN(input_size=300, # d hidden_size=512, # H num_layers=1, nonlinearity='tanh', batch_first=True, bidirectional=False) x = torch.randn(32, 100, 300) # (B, T, d) h0 = torch.zeros(1, 32, 512) # (layers·dirs, B, H) output, h_n = rnn(x, h0) # output: (32, 100, 512) every h_t, top layer # h_n: (1, 32, 512) last h_T, every layer y = W_hy(output) # readout: a separate nn.Linear
Arguments
| Argument | Sets |
|---|---|
input_size |
\(d\) |
hidden_size |
\(H\) |
num_layers |
stacked units, each with its own weights |
nonlinearity |
tanh or relu in the loop |
batch_first |
\((B, T, d)\) in and out, or \((T, B, d)\) |
bidirectional |
a second unit reading \(T\) to \(1\) |
h_0 |
the initial state, zeros by default; a learned one adds \(H\) parameters and matters for the first steps only |
dropout |
between layers only, never on the state path |
Layouts
Not in the module
output is \(\mathbf{h}_t\), and \(\mathbf{W}_{hy}\) is a separate nn.LinearParameters as PyTorch counts them
weight_ih \((H, d)\), weight_hh \((H, H)\), bias_ih and bias_hh \((H)\): two bias vectors, \(H(H + d + 2)\)output Holds Every Step and h_n Holds the Last
One output for the sequence
h_n[-1], the top layer’s final state, or output[:, -1]: the same tensorOne output per step
output, all \(T\) states of the top layer, then apply the readout to every step at oncenn.RNN applied to output adds a layerLayer \(l\) takes layer \(l - 1\)’s state as its input
Cost
Depth
Two units, two directions, one output
output is \((B, T, 2H)\), h_n is \((2L, B, H)\)Requirements
Where the loss sits
One node, two incoming gradients
Same recursion as the adaptive one-pole filter
One matrix, \(T\) contributions
Magnitude - a sum of \(T\) terms, large whenever the \(\boldsymbol{\delta}_t\) are
Sharing - every step contributes to the same weights, so a pattern learned at step 3 is applied at step 40
def rnn_backward(x, states, dLdy, W_hh, W_hy): # states: h_0 .. h_T dLdy[t]: dL_t/dy_t T = len(dLdy) dW_hh = torch.zeros_like(W_hh) dW_xh = torch.zeros(H, d) delta_next = torch.zeros(H) for t in range(T, 0, -1): delta = W_hy.T @ dLdy[t] + delta_next # two incoming terms dz = delta * (1 - states[t] ** 2) # through tanh dW_hh += torch.outer(dz, states[t - 1]) # one term per step dW_xh += torch.outer(dz, x[t]) delta_next = W_hh.T @ dz # to the previous state return dW_hh, dW_xh
Autograd
loss.backward() runs through the \(T\) copies in reverse and accumulates into one .grad per parameter
Procedure
h = h.detach(), value kept, graph cutCost
Not trained
h = h0 for chunk in sequence.split(k, dim=1): h = h.detach() # cut the graph here out, h = rnn(chunk, h) # value carried forward loss = criterion(out, targets_for(chunk)) loss.backward(); opt.step(); opt.zero_grad()
One step back
\(T - k\) steps back
Bound on each factor
Linear filter, again
Two kinds of backward step
Paths from the last output to the first input
Consequence
Measured
Horizon
Task
Trained unit
Failure
The bound above one
Symptom in a training run
nan
Spikes
Two rules
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=c)
Choosing \(c\)
Vanishing untouched
Three starting points for \(\mathbf{W}_{hh}\)
First updates only
Normalize the pre-activation, every step
Effect
Unchanged
For the gradient to survive \(T\) steps, every factor must have norm one
No such matrix
Limits of the remedies
A state path that adds instead of multiplies has a Jacobian of one by construction.
Additive path
Unconditional sum
Gates
Forget gate on the cell line
Three regimes of a gate value
Initialization
Two parts to a write
Against the plain unit
Read path
Two states
One step, in order
Given (\(H = 2\), gate values as computed by the four affine maps)
| value | |
|---|---|
| \(\mathbf{c}_{t-1}\) | \([1.5,\ -0.5]\) |
| \(\mathbf{f}_t\) | \([0.9,\ 0.1]\) |
| \(\mathbf{i}_t\) | \([0.2,\ 0.8]\) |
| \(\tilde{\mathbf{c}}_t\) | \([0.6,\ -0.4]\) |
| \(\mathbf{o}_t\) | \([0.7,\ 0.5]\) |
Gate values
Cell update
Read
Observations
| Plain unit | LSTM | GRU | |
|---|---|---|---|
| Affine maps of \([\mathbf{h}_{t-1}, \mathbf{x}_t]\) per step | 1 | 4 | 3 |
| Recurrence parameters | \(H(H + d + 1)\) | \(4H(H + d + 1)\) | \(3H(H + d + 1)\) |
| \(d = 300\), \(H = 512\) | \(416{,}256\) | \(1{,}665{,}024\) | \(1{,}248{,}768\) |
| Multiply-adds per step (recurrence) | \(H(H + d)\) | \(4H(H + d)\) | \(3H(H + d)\) |
| State carried per step | \(\mathbf{h}_t\) | \(\mathbf{h}_t\) and \(\mathbf{c}_t\) | \(\mathbf{h}_t\) |
| Activations kept for training, per step | \(\mathbf{h}_t\) | \(\mathbf{h}_t, \mathbf{c}_t\), four gate outputs | \(\mathbf{h}_t\), three gate outputs |
Cost
Unchanged
Along the cell line
Other terms
Measured on the trained units
Same task, same budget
Measured
Failed seed
Gate below one
Gate near one
Changed and unchanged
Three changes from the LSTM
Same additive path
nn.LSTM Returns the Cell State Beside h_nlstm = nn.LSTM(input_size=300, hidden_size=512, num_layers=2, batch_first=True) x = torch.randn(32, 100, 300) # (B, T, d) h0 = torch.zeros(2, 32, 512) # (layers, B, H) c0 = torch.zeros(2, 32, 512) # (layers, B, H) output, (h_n, c_n) = lstm(x, (h0, c0)) # output: (32, 100, 512) h_t of the top layer, every step # h_n, c_n: (2, 32, 512) last h and c of every layer gru = nn.GRU(300, 512, num_layers=2, batch_first=True) output, h_n = gru(x, h0) # no cell state
Weight layout, LSTM
weight_ih_l0: \((4H, d)\), weight_hh_l0: \((4H, H)\), rows in the order \(\mathbf{i}, \mathbf{f}, \mathbf{g}, \mathbf{o}\)bias_ih_l0 and bias_hh_l0: \((4H)\) each, both addedfor name, p in lstm.named_parameters(): if 'bias' in name: with torch.no_grad(): p[512:1024] = 1.0 # f rows: 1 + 1 = 2 in total
Same as nn.RNN
batch_first, num_layers, bidirectional, dropout between layersoutput is the top layer’s \(\mathbf{h}_t\), and the readout is a separate nn.LinearGRU layout
weight_ih_l0: \((3H, d)\), rows in the order \(\mathbf{r}, \mathbf{z}, \mathbf{n}\), with \(\mathbf{n}\) the candidateVariant
Origin
Comparison
Current use
nn.LSTM and absent from current reference implementations
Two readings of one unit
Both are set by the input
Tasks that do not
One vector for any length
Two consumers
Trained from where it is read
One added connection
Length set by the unit
What sets the first state
The composition
What it demands of one vector