Session S13
A loss at one step of a sequence: how far back can its gradient reach?
Deep Learning · CS 4715/5715 · University of Idaho
Quiz 2 (Module 2, S09–S12) in Canvas at the end of class, 6:45–7:15 PM.
One network, any length: a 50-word sentence applies the same update 50 times.
\(\mathbf{x}_t\) has \(d_x\) entries and \(\mathbf{h}_t\) has \(d_h\): \(W_{xh}\) is \(d_h\times d_x\), \(W_{hh}\) is \(d_h\times d_h\). A readout \(\hat{\mathbf{y}}_t=W_{hy}\,\mathbf{h}_t+\mathbf{b}_y\), with \(W_{hy}\) of shape \(K\times d_h\) for \(K\) outputs.
| read off | task | where the loss sits |
|---|---|---|
| every \(\mathbf{h}_t\) | tagging each word | a loss at every step |
| only \(\mathbf{h}_T\) | classifying the whole sentence | one loss, at the last step \(T\) |
| every \(\mathbf{h}_t\), target \(\mathbf{x}_{t+1}\) | a language model: predict the next element (this module's main case) | a loss at every step |
In code a step takes a \(B\times d_x\) input and a \(B\times d_h\) state, a row per sequence (slide 7: \(B=1\)).
In PyTorch, nn.RNNCell(32, 32) stores \(\mathbf{b}\) as two vectors that add, bias_ih + bias_hh: \(2\cdot32\cdot32+2\cdot32=2{,}112\) parameters.
Today's question: each readout puts a loss on some \(\mathbf{h}_t\). How far back can that loss's gradient reach?
To answer the question, follow the gradient back from a loss. First, how do you train a network that loops?
A loss at step \(T\) reaches step \(t\) only back through every state in between, a step-by-step relay. The next pages show two ways it is cut.
The shape of input and output decides where the loss sits; the next pages follow its gradient back.
The first reason a gradient stops short: the graph is cut on purpose. Nobody unrolls a whole book: memory and compute grow with the number of unrolled steps. With a loss at every step, a long text is read as one stream and cut every 128 steps: backpropagate and update at the end of each window, then pass the state on detached.
h = h.detach().Token 7's target is token 3's symbol, four steps back; dashed lines are window boundaries.
Pick the window longer than the longest dependency the model must learn.
Excerpt of reading 3.1's train(window): cell, a long short-term memory (LSTM) cell, nn.LSTMCell(4, 64), carrying two states h and c (slide 15), and a linear head read one-hot symbols (V = 4 kinds, chance 0.250); the target is the symbol from LAG = 4 steps back, and history holds the last LAG symbols.
for w in range(n_windows):
h, c = h.detach(), c.detach() # carry the state, not the gradient
loss = 0.0
for _ in range(window):
sym = torch.randint(0, V, (1,))
x = torch.zeros(1, V); x[0, sym] = 1.0
h, c = cell(x, (h, c))
logits = head(h)
target = history[0].view(1) # the symbol from LAG steps ago
loss = loss + nn.functional.cross_entropy(logits, target)
history = torch.cat([history[1:], sym])
opt.zero_grad(); loss.backward(); opt.step()
dependency length LAG = 4, chance accuracy = 0.250
truncation window 2 (window <= LAG): accuracy = 0.315
truncation window 4 (window <= LAG): accuracy = 0.320
truncation window 8 ( window > LAG): accuracy = 0.775
truncation window 16 ( window > LAG): accuracy = 0.999
n_windows = 1,200 windows each, opt = Adam; accuracy over the last 100.Inside a window, the gradient still crosses every step: a product. S04's two rules decide what reaches step \(t\).
A dependency \(T-t\) steps long is learned through a product of \(T-t\) factors.
Derivation. Prerequisites: S04's series rule and tanh row. One hidden unit, so every Jacobian is a number.
Linear case: \(\lvert w\rvert<1\) vanishes, \(\lvert w\rvert>1\) explodes. With tanh, no factor exceeds \(\lvert w\rvert\).
Reading 3.1's script, Part 1: the scalar case from \(t=1\), so the exponent is \(T-1\).
print("Linear recurrence: dh_T/dh_1 = w^(T-1)")
for w in [0.5, 0.9, 1.0, 1.1, 2.0]:
print(f" w = {w:>4}: T=5 -> {w**4:.4f} T=20 -> {w**19:.3e} T=50 -> {w**49:.3e}")
Linear recurrence: dh_T/dh_1 = w^(T-1)
w = 0.5: T=5 -> 0.0625 T=20 -> 1.907e-06 T=50 -> 1.776e-15
w = 0.9: T=5 -> 0.6561 T=20 -> 1.351e-01 T=50 -> 5.726e-03
w = 1.0: T=5 -> 1.0000 T=20 -> 1.000e+00 T=50 -> 1.000e+00
w = 1.1: T=5 -> 1.4641 T=20 -> 6.116e+00 T=50 -> 1.067e+02
w = 2.0: T=5 -> 16.0000 T=20 -> 5.243e+05 T=50 -> 5.629e+14
\(w=0.9\): about 0.006 at \(T=50\), about 100× below the 5-step 0.66. \(w=2.0\): 5.6e14. Only \(w=1.0\) keeps the size; training does not hold a weight there.
Predict for \(h_t=\tanh(w\,h_{t-1}+x_t)\), \(h_0=0\), small random \(x_t\) (std 0.1), \(w=2.0\), \(T=50\): about \(5.6\times10^{14}\), about 1, or near 0?
Answer: near 0. Each factor is \(w\,(1-h_k^2)\) (slide 9, step 3). Follow one run: \(w=2.0\), \(h_0=0\), \(x_k\) with std 0.1.
| \(k\) | \(x_k\) | \(h_k\) | \(1-h_k^2\) | factor \(2(1-h_k^2)\) |
|---|---|---|---|---|
| 1 | +0.188 | 0.186 | 0.965 | 1.93 |
| 2 | −0.135 | 0.233 | 0.946 | 1.89 |
| 3 | −0.127 | 0.327 | 0.893 | 1.79 |
| 4 | +0.097 | 0.635 | 0.596 | 1.19 |
| 5 | −0.117 | 0.819 | 0.329 | 0.66 |
| 6 | +0.194 | 0.950 | 0.097 | 0.19 |
Not one factor near 0: 45 factors near 0.16.
Numbers from reading 3.1's script, Part 2, seed 0.
The same script for three weights (reading 3.1's script, Part 2, seed 0): from \(h_0=0\), the product of the first \(T\) factors \(w\,(1-h_k^2)\).
tanh recurrence: measured |dh_T/dh_0| (product of T per-step factors)
w = 0.9: T=5 -> 0.3031 T=20 -> 1.066e-02 T=50 -> 1.823e-04
w = 1.1: T=5 -> 1.3783 T=20 -> 4.979e-02 T=50 -> 1.477e-03
w = 2.0: T=5 -> 5.1259 T=20 -> 1.806e-12 T=50 -> 4.013e-36
Exploding has a one-line patch (next page); vanishing needs the LSTM (slide 15).
Slides 9–12: the product can vanish or explode. Exploding first, since it has a one-line patch; vanishing waits for the LSTM (slide 15). Gradient clipping (Pascanu, Mikolov & Bengio 2013; reading 2.4).
loss.backward() and before opt.step(): if the gradient's norm \(\lVert g\rVert\) exceeds a threshold \(c\), rescale it to norm \(c\):
opt.zero_grad(); loss.backward()
gnorm = torch.nn.utils.clip_grad_norm_(params, clip).item()
max_gnorm = max(max_gnorm, gnorm)
opt.step()
clip_grad_norm_ rescales the gradients in place and returns their norm before clipping, the raw norm; clip is 1.0, or float("inf") for no clipping.zero_grad() and backward(), it finds no gradients and returns 0.0.Reading 3.1's run: a vanilla RNN reads 60 random numbers and outputs their sum. Carrying the running total undiminished pushes \(W_{hh}\) toward slide 10's \(\lvert w\rvert\ge1\) rows, so the task invites explosion. Always predicting 0 scores a loss of about 60 (a sum of 60 standard normals has variance 60). Same seed, data and learning rate; only the clipping differs.
--- no clipping (largest raw grad norm seen: inf)
steps 1-100: median loss = nan
steps 101-200: median loss = nan
steps 201-300: median loss = nan
steps 301-400: median loss = nan
--- clip at 1.0 (largest raw grad norm seen: 7806.4)
steps 1-100: median loss = 45.3301
steps 101-200: median loss = 35.3441
steps 201-300: median loss = 32.7797
steps 301-400: median loss = 34.6408
Clipping bounds the update; it cannot raise a gradient at the tanh table's \(10^{-36}\). That needs a different architecture.
Within the vanilla RNN, vanishing has no patch (slide 12's trade-off); the fix is a different update. Long short-term memory (LSTM): Hochreiter & Schmidhuber 1997. The forget gate \(\mathbf{f}\) was added in 2000 (Gers, Schmidhuber & Cummins); the 1997 paper had none. \([\mathbf{h},\mathbf{x}]\) is the concatenation of \(\mathbf{h}_{t-1}\) and \(\mathbf{x}_t\).
| gate | computed as | what it does |
|---|---|---|
| forget | \(\mathbf{f}=\sigma(W_f[\mathbf{h},\mathbf{x}]+\mathbf{b}_f)\) | the fraction of each cell entry to keep |
| input | \(\mathbf{i}=\sigma(W_i[\mathbf{h},\mathbf{x}]+\mathbf{b}_i)\) | how much of \(\mathbf{g}\) to write |
| candidate | \(\mathbf{g}=\tanh(W_g[\mathbf{h},\mathbf{x}]+\mathbf{b}_g)\) | the content that could be written |
| output | \(\mathbf{o}=\sigma(W_o[\mathbf{h},\mathbf{x}]+\mathbf{b}_o)\) | how much of the cell to expose |
The cell state is updated by gated addition, not by a matrix product and a tanh.
① forget: keep the fraction \(\mathbf{f}\) of each cell entry. ② write: add \(\mathbf{i}\odot\mathbf{g}\), how much of the candidate \(\mathbf{g}\) to write. ③ expose: \(\mathbf{h}_t=\mathbf{o}\odot\tanh(\mathbf{c}_t)\), how much of the cell to expose.
The gated recurrent unit (GRU, Cho et al. 2014) is a two-gate variant without a separate cell: 3 weight blocks instead of 4; same idea, an additive gated path.
Read the cell left to right: keep a fraction, add new content, expose part of it.
With \(\mathbf{f}\) near 1, the gradient passes back almost undamped, for as many steps as the gate stays open.
Slide 17's condition is \(\mathbf{f}\) near 1. At initialization \(W_f[\mathbf{h},\mathbf{x}]\) is small, so \(\mathbf{f}\approx\sigma(\mathbf{b}_f)\): the forget bias sets where \(\mathbf{f}\) starts. Where does PyTorch set it? Input and hidden size 32, as in the measurement that follows; same seed, so the same LSTM initialization.
import torch
from torch import nn
torch.manual_seed(0)
lstm, rnn = nn.LSTMCell(32, 32), nn.RNNCell(32, 32)
for name, p in lstm.named_parameters():
print(name, tuple(p.shape))
print(sum(p.numel() for p in lstm.parameters()),
sum(p.numel() for p in rnn.parameters()))
b_f = lstm.bias_ih[32:64] + lstm.bias_hh[32:64] # the forget slice
print(f"forget-gate bias: {b_f.min():.3f} to {b_f.max():.3f}")
weight_ih (128, 32)
weight_hh (128, 32)
bias_ih (128,)
bias_hh (128,)
8448 2112
forget-gate bias: -0.311 to 0.271
Does the gradient reach further? Predict, then measure.
Reading 3.1's experiment, seed 0: input and hidden size 32, \(T\) random inputs \(\mathbf{x}_1,\dots,\mathbf{x}_T\), the loss h.sum() on the last hidden state.
nn.RNNCell(32, 32); nn.LSTMCell(32, 32) as initialized; the same LSTM with forget-gate bias 2 (fb=2).bias_ih and bias_hh, so \(\mathbf{f}\) starts near \(\sigma(2)\approx0.88\).Predict the order of the three columns at \(T=80\): which cell keeps the most gradient, and which the least? Roughly how far apart?
The printout (reading 3.1): gradient norm at the first input.
T RNN LSTM LSTM fb=2
5 9.51e-02 1.13e-01 4.62e-01
10 2.06e-03 9.79e-03 2.76e-01
20 2.86e-07 3.54e-05 1.54e-01
40 4.71e-14 1.64e-09 3.48e-02
80 5.47e-29 5.27e-18 3.12e-03
5e-29 at \(T=80\). No float32 weight moves by that much: the first input is invisible to learning.3e-3 reaches the first input at \(T=80\).Gating hands the decay rate to the model, and to your initialization. In a real run you see these cuts only as symptoms.
| symptom | cause from today | check or fix |
|---|---|---|
| the loss becomes NaN | exploding gradients | clip from the start, after backward() and before step(); a smaller learning rate |
| "Trying to backward through the graph a second time" across batches | the carried state still holds the previous window's graph | h, c = h.detach(), c.detach() at every truncation boundary; not retain_graph=True, which only trades the error for "modified by an inplace operation" |
| the model uses only the last few tokens | vanishing gradients, or a window no longer than the dependency | an LSTM or a gated recurrent unit (GRU), a hand-set forget bias, a longer window |
| clipping on, loss stable, long-range dependencies not learned | clipping hides vanishing | gradient norms at different lags, as S10 printed per-layer norms |
| sequences padded to one length, the padding processed as real input | the cell updates its state on pad tokens | pack_padded_sequence (gives the cell each sequence's true length), or read the state at each sequence's true last step |
Every fix here keeps the relay and the serial loop.
Next session (S14): attention.
(a) Your loss is smooth and never NaN, but the LSTM never learns a dependency 20 tokens long. The truncation window is 16. What do you change first?
(b) clip_grad_norm_(params, 1.0) stopped the NaNs. Are the long-range gradients now healthy?
(c) Why does forget bias 2 keep 3e-3 at \(T=80\) when the default LSTM keeps 5e-18?
(a) The window: no gradient crosses a boundary, so no dependency of 16 or more steps is learned (lag 4: window 4 gave 0.320, window 16 gave 0.999, slide 7). Make it longer than the dependency.
(b) Not necessarily: clipping bounds the update; it cannot raise a gradient at 4e-36 (slide 11). Check gradient norms at different lags.
(c) Along the cell, each step multiplies by \(\mathrm{diag}(\mathbf{f})\); bias 2 starts \(\mathbf{f}\) near \(\sigma(2)\approx0.88\) instead of about 0.5 (slides 17 and 20).
Everyone completes Quiz 2 in Canvas.
Covers Module 2: S09–S12.
Online (Web) students and students with flexible-time arrangements may use their assigned flexible completion window.
Closed materials (Tier 0). No late quizzes; make-ups only in the syllabus's three cases.
Before next session (S14): read 3.2 Attention. S14 lets step 300 look directly at step 3; computing every step at once comes with the Transformer (S16).