Zhanbo Chen
Back to home

CS546 Learning Notes: Connecting Gradient Clipping to LSTM and GRU Through a Parameter Update

Algorithm/Learning Notes
2026-09-13
13 min read
0 reads
AIMachine Learning

In brief

Last time I learned about RNN's BPTT and understood that gradients multiply continuously as they backpropagate through time, calculating 0.25=0.000320.2^5=0.00032. But when I encountered gradient clipping, I realized I hadn't truly understood: what's the actual problem with gradient explosion, and why do we need to clip? Starting from this question, I gradually understand the gating mechanisms of LSTM and GRU, and finally connect the division of labor between BPTT and the Optimizer. The most rewarding insight is clarifying several easily confused concepts: memory and output, gate values and parameters, multiplication along paths and addition across multiple paths.

1. Gradient Explosion: Why Large Gradients Can Lead Training Astray?

First, let's look at vanilla SGD without momentum or weight decay:

wnew=w−ηg,g=∂L∂ww_{\text{new}}=w-\eta g,\qquad g=\frac{\partial L}{\partial w}

ww is the parameter, η\eta is the learning rate, and gg is the gradient. Suppose w=1w=1 and η=0.1\eta=0.1:

GradientUpdated ParameterUpdate Magnitude
221−0.1×2=0.81-0.1\times2=0.80.20.2
100010001−0.1×1000=−991-0.1\times1000=-99100100

At the time, I calculated that the two update magnitudes differed by 99.899.8, and I realized that in the second case, a single step went too far.

The gradient describes changes near the current position. Taking a sufficiently small step along the negative gradient direction usually decreases the differentiable loss; taking a large step offers no such guarantee. Therefore, a large gradient combined with a fixed learning rate may cause parameters to overstep the appropriate position, actually increasing the loss.

Why does this happen easily in RNNs? Consider a scalar example stripped of inputs and activation functions:

ht=wht−1,∂ht∂ht−1=wh_t=w h_{t-1},\qquad \frac{\partial h_t}{\partial h_{t-1}}=w

If the terminal gradient is 11 and w=3w=3, backpropagating through three steps gives:

∂L∂h0=1×33=27\frac{\partial L}{\partial h_0}=1\times3^3=27

After ten steps, the same multiplicative factor is 310=590493^{10}=59049. This computes the gradient with respect to hidden state; when computing parameter gradients, these backpropagation signals are also used, so parameter gradients may correspondingly grow large.

"Increasing time steps" refers to the backpropagation path spanning more sequence positions, not that increased training epochs necessarily cause explosion. Real vector RNNs involve Jacobian multiplication, and not all paths continuously amplify. This scalar example demonstrates one mechanism.

2. Gradient Clipping: Limit the Length, Preserve the Direction

Initially, I understood clipping as "filtering outliers." More precisely: limit the impact of excessively large gradients, not discard them. Large gradients themselves don't necessarily indicate problematic data.

Organize the gradient to be clipped into a vector gg, and set the maximum length as c>0c>0. L2 norm clipping is:

g~={c∥g∥2g,∥g∥2>cg,∥g∥2≤c\tilde g= \begin{cases} \dfrac{c}{\|g\|_2}g,&\|g\|_2>c\\ g,&\|g\|_2\le c \end{cases}

For example, g=[3,4]g=[3,4] has length 32+42=5\sqrt{3^2+4^2}=5. With a threshold of 22:

g~=25[3,4]=[1.2,1.6]\tilde g=\frac25[3,4]=[1.2,1.6]

All components are multiplied by the same positive number, so the direction remains unchanged and the length is scaled to 22. If we directly changed it to [2,2][2,2], the ratio would change from 3:43:4 to 1:11:1, altering the direction; that is not norm clipping.

Lecture 5, page 44 of the course introduces this method. The referenced paper by Pascanu et al. proposes using gradient norm clipping to address gradient explosion.

Why Not Simply Lower the Learning Rate?

My initial thought was: "Lowering the learning rate only suppresses early updates; it will still explode later." This needs correction: a smaller learning rate shrinks every update, not just the first few. The real difference is that it also shrinks updates from normally-sized gradients.

ApproachUpdate Magnitude When g=2g=2Update Magnitude When g=1000g=1000
Learning rate 0.0010.0010.0020.00211
Learning rate 0.10.1, clip threshold 550.20.20.50.5

The above still assumes vanilla SGD. Clipping only intervenes when a threshold is exceeded, while a small learning rate uniformly shrinks all updates. The two can also be used together.

Clipping occurs after gradient computation and does not cancel the chain multiplications in backpropagation. It also cannot solve gradient vanishing: 0.000010.00001 is below the threshold 55, and after clipping remains 0.000010.00001.

3. LSTM: Retain Old Memories While Writing New Content

Gradient vanishing is not just about "small steps." When loss from later steps propagates to earlier positions, the signal becomes too weak, making it difficult for the model to learn to use information from the distant past.

LSTM maintains a cell state ctc_t and updates memory through gates:

ct=ft⊙ct−1+it⊙c~tc_t=f_t\odot c_{t-1}+i_t\odot\tilde c_t
SymbolMeaning
ct−1c_{t-1}Old memory
ftf_tForget gate, how much old memory to retain
c~t\tilde c_tCandidate memory, content to be written
iti_tInput gate, how much candidate content to write

⊙\odot denotes element-wise multiplication. In practice, these quantities are usually vectors, with each component having its own gate value; we'll compute one component by hand first.

Old memory is 88, forget gate is 0.750.75, retaining 66. Candidate memory is 0.80.8, input gate is 0.50.5:

ct=0.75×8+0.5×0.8=6.4c_t=0.75\times8+0.5\times0.8=6.4

Here ctc_t can exceed 11; the candidate memory component after tanh is within (−1,1)(-1,1). This example with 88 assumes memory has accumulated previously.

Are Gate Values Manually Fixed?

I then asked: "How are the input gate values defined? Is the forget gate a fixed value?"

They are computed from inputs, context, and trainable parameters:

it=σ(Wixt+Uiht−1+bi)i_t=\sigma(W_i x_t+U_i h_{t-1}+b_i) ft=σ(Wfxt+Ufht−1+bf)f_t=\sigma(W_f x_t+U_f h_{t-1}+b_f)

σ\sigma is sigmoid, mapping finite input to (0,1)(0,1) as a passage ratio. For example, when sigmoid input is −2,0,2-2, 0, 2 respectively, outputs are approximately 0.12,0.5,0.880.12, 0.5, 0.88.

Here we use column vector convention: if input dimension is dd and hidden dimension is mm, then xt∈Rdx_t\in\mathbb R^d, ht−1,ct,it,ft∈Rmh_{t-1},c_t,i_t,f_t\in\mathbb R^m, Wi∈Rm×dW_i\in\mathbb R^{m\times d}, Ui∈Rm×mU_i\in\mathbb R^{m\times m}, bi∈Rmb_i\in\mathbb R^m; similarly for other gates.

The same set of parameters is shared across all time steps, but since inputs and contexts differ, gate values can be different. Training updates these parameters; gate values are computed results from each forward pass.

4. Output Gate: Memory Retained Doesn't Mean It's Presented Now

LSTM also has an output gate:

ot=σ(Woxt+Uoht−1+bo)o_t=\sigma(W_o x_t+U_o h_{t-1}+b_o) ht=ot⊙tanh⁡(ct)h_t=o_t\odot\tanh(c_t)

I could compute 0.25×0.8=0.20.25\times0.8=0.2, but then asked again: "What exactly does this hth_t represent?"

Now I've clarified it: ctc_t is internal memory, hth_t is the current hidden state. hth_t can be sent to the output layer for predictions and participates in computing next step's gate values and candidate content; ctc_t continues along the memory path. The standard LSTM's state relationships can be compared with PyTorch LSTM formula.

For example, a language model could compute logitst=Wyht+by\text{logits}_t=W_yh_t+b_y, then apply Softmax to predict the next word. hth_t itself is neither a word nor a probability.

When the output gate is close to 00, the corresponding hth_t component is close to zero, but ctc_t won't be directly cleared. To retain a memory, not present it for now, and not write new content, we can set the corresponding:

ft≈1,it≈0,ot≈0f_t\approx1,\qquad i_t\approx0,\qquad o_t\approx0

I also corrected a statement: "When the output gate opens, output follows the old memory." In reality, the output gate acts on the updated ctc_t. The memory just changed to 6.46.4, so the ideal case with output gate 11 corresponds to tanh⁡(6.4)\tanh(6.4), not tanh⁡(6)\tanh(6).

To preserve memory approximately unchanged, besides ft≈1f_t\approx1, the new addition term should be near zero, e.g., it≈0i_t\approx0. Simply keeping the forget gate near 11 could still allow continued writing of new content.

5. Why Can LSTM Alleviate Gradient Vanishing?

Along the path from old memory directly to new memory, the direct local derivative of each component is the corresponding ftf_t component. Therefore, when five consecutive steps are 0.990.99, the gradient factor along this path is:

0.995≈0.9510.99^5\approx0.951

Compared to 0.25=0.000320.2^5=0.00032, the signal is preserved much better. My understanding is: "ftf_t close to 11 better preserves old information; multiplying by 0.20.2 each step gradually causes forgetting." Plus backpropagation: gradients also more easily propagate back along this direct path to earlier positions.

This "direct" is important. The complete computation graph also includes dependencies through ht−1h_{t-1} from gate values and candidate content; we can't reduce the complete derivative to just forget gate multiplication. LSTM provides more favorable propagation paths, but doesn't guarantee gradients never vanish or explode.

6. GRU: Blend Old and Candidate States Using an Update Gate

GRU doesn't maintain ctc_t separately, but updates hth_t. This article adopts PyTorch GRU convention:

ht=zt⊙ht−1+(1−zt)⊙nth_t=z_t\odot h_{t-1}+(1-z_t)\odot n_t

ztz_t is the update gate, ntn_t is the candidate state. Here, the closer ztz_t is to 11, the more old state is retained; the closer to 00, the more candidate state is used.

Some materials have ztz_t multiply the candidate state, with opposite notation. When reading, check the formula, not just the gate name.

For example, old state is 0.80.8, candidate state is 0.20.2, zt=0.75z_t=0.75:

ht=0.75×0.8+0.25×0.2=0.65h_t=0.75\times0.8+0.25\times0.2=0.65

At the time, I summarized it as "either retain more old state or retain more new input." Here we need to change "new input" to "candidate state": it's not the original xtx_t, but computed content that can itself contain historical information.

7. What's the Difference Between Reset Gate and Forget Gate?

The reset gate rtr_t in GRU appears in the generation of candidate state. Continuing with PyTorch notation:

nt=tanh⁡(Winxt+bin+rt⊙(Whnht−1+bhn))n_t=\tanh\left(W_{in}x_t+b_{in}+r_t\odot(W_{hn}h_{t-1}+b_{hn})\right)

It is multiplied onto the contribution of the old state after transformation. When rt≈0r_t\approx0, the candidate content references history less; when rt≈1r_t\approx1, this portion of historical contribution is allowed to pass through. PyTorch's reset gate placement differs from some original implementations, and this article consistently uses the version above.

The name "reset" initially led me to guess backwards, thinking that wanting to start fresh should mean close to 11. Only by looking at the position did I realize that here the value represents the passage ratio.

I later asked: "Is its function similar to the LSTM forget gate?" Both control historical information, but at different positions:

GatePosition Controlled
LSTM ftf_tPath where old memory is directly retained to new memory
GRU rtr_tContribution of old state when generating candidate state
GRU ztz_tRetention ratio of old state in final mixing

So in terms of "directly retaining the old state," ztz_t in this article's convention is more similar to ftf_t.

Two combinations helped me truly distinguish them: when rt≈0,zt≈1r_t\approx0,z_t\approx1, the candidate content references history less, but the final state still mainly retains the old state; when rt≈1,zt≈0r_t\approx1,z_t\approx0, the final state mainly adopts the candidate state, but the candidate state may still contain processed historical information. Allowing reference to history does not guarantee preserving it unchanged.

8. BPTT: When to Multiply, When to Add?

Gate parameters are learned from task loss; no need to manually annotate correct values for each gate. But does BPTT update parameters at each step every time it backpropagates?

Usually not. Parameters are shared across time steps; BPTT collects and accumulates gradient contributions, which are then updated by the optimizer.

In the Q&A, two time steps' contributions to the same parameter were 0.30.3 and −0.1-0.1, which I mistakenly computed as the product −0.03-0.03. The correct result should be:

0.3+(−0.1)=0.20.3+(-0.1)=0.2

The rule remains: along the same path, local derivatives multiply; when multiple paths converge to the same parameter, contributions add.

Even with loss only from the second step, the shared parameter ww has different paths:

h1=F(x1,h0;w),h2=F(x2,h1;w)h_1=F(x_1,h_0;w),\qquad h_2=F(x_2,h_1;w)

One path affects loss through w→h2→L2w\rightarrow h_2\rightarrow L_2, another through w→h1→h2→L2w\rightarrow h_1\rightarrow h_2\rightarrow L_2. Compute derivatives along each path, then add them.

"Different time steps are different paths" is an introductory understanding. More precisely, parameter usages at different time steps form different paths, and each usage may affect multiple subsequent losses. The gradient contribution of a particular step must already include relevant subsequent propagation; it cannot be omitted or double-counted.

9. Optimizer: Actually Modifying Parameters After Receiving Gradients

BPTT is responsible for computing gradients; the Optimizer is responsible for using gradients to update parameters. The simplest example is still ordinary SGD:

wnew=w−ηgw_{\text{new}}=w-\eta g

Contributions from three time steps are 0.3,−0.1,0.40.3,-0.1,0.4; total gradient is 0.60.6. If the parameter is 11 and learning rate is 0.10.1:

wnew=1−0.1×0.6=0.94w_{\text{new}}=1-0.1\times0.6=0.94

In the next round, if the gradient is −0.4-0.4:

wnew=0.94−0.1×(−0.4)=0.98w_{\text{new}}=0.94-0.1\times(-0.4)=0.98

Negative gradient causes the parameter to increase. The updated parameters are used in the forward computation of all time steps in the next iteration. This computation corresponds to SGD without momentum, weight decay, or other extra terms.

Momentum and Adam further utilize historical information to determine updates, but here I've only established the optimizer's responsibility and basic SGD computation without expanding these algorithms.

10. The Training Loop Connected This Round

A typical training iteration can be read straight through: clear previous gradients → compute states, gate values, and predictions in forward pass → compute task loss → BPTT computes and accumulates gradients of shared parameters → clip if necessary → Optimizer updates parameters.

In practice there are also arrangements like gradient accumulation across batches and truncated BPTT, so "only update after the entire sequence ends" shouldn't be treated as the only approach. The core division of labor remains unchanged: computing derivatives and updating parameters are separate steps.

Three points I most want to remember from this round: clipping limits overly large gradients but cannot restore vanished signals; gating allows the model to learn how to retain and use history, but gate values are not parameters; during backpropagation, multiply along paths and add when summing contributions to shared parameters.

Next time I see a complex formula, I'll ask first: which path does this quantity control? Is it a state computed right now, or a parameter to be updated during training? This is more reliable than just memorizing gate names.

ZB

Zhanbo Chen

Java Backend & AI Agent Developer

Back to home
Comments
CS546 Learning Notes: Connecting Gradient Clipping to LSTM and GRU Through a Parameter Update | Zhanbo