๐Ÿ”„ RNN & LSTM

Memory across time, why its gradient dies, and how gates rescue it
Neural Networks Advanced
Contents

    A dense network sees a fixed-size input and has no notion of "before". A convolution sees a fixed window. Neither can answer "has this pattern been building up for the last two seconds?" — and for EEG, speech, or text, that is usually the question.

    A recurrent network adds one thing: a hidden state carried from one timestep to the next.

    $$\mathbf{h}_t = \tanh\big(W_x\mathbf{x}_t + W_h\mathbf{h}_{t-1} + \mathbf{b}\big)$$

    That is the entire architecture. $\mathbf{h}_t$ is a summary of everything seen so far, $\mathbf{h}_{t-1}$ carries it forward, and the same weights are used at every timestep — parameter sharing across time rather than across space.

    It also has a specific, famous failure. Because the same $W_h$ is applied at every step, gradients flowing back through $T$ steps are multiplied by $W_h$ roughly $T$ times, so they vanish or explode geometrically. That failure is measured below, and it is exactly what the LSTM was invented to fix.

    Display helpers ready: banner, step, check, expand, show_matrix, show_matmul, show_elementwise.
    Network diagram helper ready: draw_network(layer_sizes, Ws, bs, ...).
       edge width = |weight|,  blue = positive,  red = negative

    1. The Sequence

    A short EEG-like sequence: 6 timesteps, 2 features per step (say band power and line length). The task is to say whether a seizure is underway by the end — a decision that depends on the history, not on the final timestep alone.

    one sequence: 6 timesteps x 2 features
    
      step     band_power    line_length
    ----------------------------------------
        t0           0.20           0.10
        t1           0.25           0.15
        t2           0.30           0.20
        t3           0.90           0.80   <- onset
        t4           1.10           0.95
        t5           1.20           1.05
    ----------------------------------------
    
    target: 1 (seizure)   - a property of the WHOLE sequence
    
    Note what a plain feed-forward net would have to do: either look at one
    timestep (losing the history) or flatten all 6 into a 12-long vector,
    which fixes the length forever and learns each position separately.

    2. The RNN Cell, Unrolled

    The cell is applied once per timestep, always with the same weights:

    $$\mathbf{h}_t = \tanh\big(W_x\mathbf{x}_t + W_h\mathbf{h}_{t-1} + \mathbf{b}\big), \qquad \mathbf{h}_{-1} = \mathbf{0}$$

    Drawing it "unrolled" makes the structure obvious: it is a deep network whose depth equals the sequence length, and whose layers are forced to share one weight matrix.

    weights - and there is only ONE set, reused at every timestep
    
    Wx  (input -> hidden) (2, 3) =
                         h0        h1        h2
       band_power |   +1.225    -1.533    +0.251
       line_length |   -0.341    -0.272    -0.129
    
    Wh  (hidden -> hidden) (3, 3) =
                         h0        h1        h2
               h0 |   -1.212    -0.139    -0.519
               h1 |   +1.994    +0.135    -0.212
               h2 |   -0.169    -0.401    -0.633
    
    parameters: Wx 6 + Wh 9 + bh 3 + Wy 3 + by 1 = 22
    and that count does NOT change if the sequence becomes 1000 steps long.
    
    
    unrolling, one timestep at a time
    ==============================================================================
    
    --- t = 0 ---   x_t = [0.2 0.1]
       x_t @ Wx        = [ 0.2108 -0.3338  0.0372]
       h_prev @ Wh     = [0. 0. 0.]      (h_prev = [0. 0. 0.])
       z = sum + bias  = [ 0.2108 -0.3338  0.0372]
       h_t = tanh(z)   = [ 0.2078 -0.322   0.0372]
    
       worked element h_0[0]:
          (+0.2000)*(+1.2246) + (+0.1000)*(-0.3407)
          = +0.210844   (h_prev is all zeros, adds nothing)
          tanh(+0.210844) = +0.207774   MATCH: True
    
    --- t = 1 ---   x_t = [0.25 0.15]
       x_t @ Wx        = [ 0.255  -0.4241  0.0433]
       h_prev @ Wh     = [-0.9    -0.0874 -0.0633]      (h_prev = [ 0.2078 -0.322   0.0372])
       z = sum + bias  = [-0.645  -0.5115 -0.02  ]
       h_t = tanh(z)   = [-0.5683 -0.4711 -0.02  ]
    
    --- t = 2 ---   x_t = [0.3 0.2]
       x_t @ Wx        = [ 0.2992 -0.5143  0.0494]
       h_prev @ Wh     = [-0.2472  0.0233  0.4074]      (h_prev = [-0.5683 -0.4711 -0.02  ])
       z = sum + bias  = [ 0.052  -0.4911  0.4567]
       h_t = tanh(z)   = [ 0.052  -0.4551  0.4274]
    
    --- t = 3 ---   x_t = [0.9 0.8]
       x_t @ Wx        = [ 0.8296 -1.5973  0.1223]
       h_prev @ Wh     = [-1.0424 -0.2402 -0.2013]      (h_prev = [ 0.052  -0.4551  0.4274])
       z = sum + bias  = [-0.2129 -1.8375 -0.079 ]
       h_t = tanh(z)   = [-0.2097 -0.9506 -0.0788]
    
    --- t = 4 ---   x_t = [1.1  0.95]
       x_t @ Wx        = [ 1.0234 -1.9447  0.1531]
       h_prev @ Wh     = [-1.6278 -0.068   0.3599]      (h_prev = [-0.2097 -0.9506 -0.0788])
       z = sum + bias  = [-0.6044 -2.0127  0.5129]
       h_t = tanh(z)   = [-0.5402 -0.9649  0.4722]
    
    --- t = 5 ---   x_t = [1.2  1.05]
       x_t @ Wx        = [ 1.1118 -2.1252  0.1652]
       h_prev @ Wh     = [-1.3489 -0.2448  0.1856]      (h_prev = [-0.5402 -0.9649  0.4722])
       z = sum + bias  = [-0.2371 -2.3701  0.3508]
       h_t = tanh(z)   = [-0.2328 -0.9827  0.3371]
    
    ==============================================================================
    
    final hidden state h_5 = [-0.2328 -0.9827  0.3371]
    output = sigmoid(h @ Wy + by) = 0.430987   (target 1)
    
    
    the hidden state over time - watch it move when the onset arrives
    
      step           h0           h1           h2      ||h||     change
    --------------------------------------------------------------
      init     0.000000     0.000000     0.000000     0.0000           
        t0     0.207774    -0.321966     0.037219     0.3850     0.3850
        t1    -0.568294    -0.471142    -0.019990     0.7385     0.7923
        t2     0.051964    -0.455064     0.427425     0.6265     0.7650
        t3    -0.209699    -0.950558    -0.078842     0.9766     0.7552
        t4    -0.540162    -0.964917     0.472238     1.2024     0.6427
        t5    -0.232767    -0.982677     0.337088     1.0646     0.3363
    --------------------------------------------------------------
    
    The biggest jump is at t3 - the step where the input changed. The state
    is accumulating a summary, not just reacting to the current input.
    unrolled, the RNN is a deep network with tied weights:
    
    Every hidden-to-hidden arrow above is the SAME matrix Wh. That is what
    makes it recurrent - and, as the next section shows, what makes the
    gradient behave so badly.

    3. Backpropagation Through Time

    Training an RNN means unrolling it and running ordinary backpropagation over the unrolled graph — BPTT. One thing is different from a feed-forward net: since $W_h$ is used at every step, its gradient is the sum of the gradients from every step.

    $$\frac{\partial\mathcal{L}}{\partial W_h} = \sum_{t=0}^{T-1}\frac{\partial\mathcal{L}}{\partial W_h}\bigg|_{\text{step }t}$$

    And the blame at step $t$ arrives from step $t+1$ through $W_h$, scaled by the local tanh derivative:

    $$\delta_t = \big(\delta_{t+1}W_h^\top\big)\odot\big(1 - \mathbf{h}_t^2\big)$$

    That recursion is the source of everything that follows.

    forward: y_hat = 0.430987, target = 1, loss = 0.841678
    
    blame travelling backwards through time
    
      step    ||delta_t||    relative to last step
    ------------------------------------------------
        t5     0.14549277               100.0000%
        t4     0.14433058                99.2012%
        t3     0.18631693               128.0592%
        t2     0.36265026               249.2566%
        t1     0.38179738               262.4167%
        t0     0.35439859               243.5850%
    ------------------------------------------------
    
    Each step back multiplies by Wh and by (1 - h^2). The tanh derivative is
    at most 1 and usually well below it, so the blame shrinks as it travels.
    
       blame at the last step  : 0.14549277
       blame at the first step : 0.35439859
       ratio                   : 2.435850
    
    Over 6 steps that is survivable. Over 100 it is not - see section 4.
    
    
    gradient check against central differences (all 22 parameters)
    
           param         analytic        numerical      rel err
    ----------------------------------------------------------
        Wx[0, 0]      0.109590534      0.109590534     2.11e-11
        Wx[0, 1]      0.046428179      0.046428179     1.56e-10
        Wx[0, 2]      0.076996981      0.076996981     3.02e-10
        Wx[1, 0]      0.113044493      0.113044493     1.02e-10
        Wx[1, 1]      0.023828827      0.023828827     1.18e-10
        Wx[1, 2]      0.064431474      0.064431474     1.89e-10
             ...
    ----------------------------------------------------------
    
    parameters checked : 22
    worst relative error: 3.266e-09   -> CORRECT

    4. Vanishing and Exploding Gradients

    The recursion $\delta_t = (\delta_{t+1}W_h^\top)\odot(1-\mathbf{h}_t^2)$ applied $T$ times means the gradient reaching step 0 has been multiplied by roughly $W_h^T$.

    $$\left\lVert\frac{\partial \mathcal{L}}{\partial \mathbf{h}_0}\right\rVert \;\sim\; \lambda^{T}, \qquad \lambda = \text{largest singular value of } W_h$$

    • $\lambda < 1$ → the gradient vanishes: early timesteps receive nothing, so the network cannot learn long-range dependencies. It is not slow — it is silent.
    • $\lambda > 1$ → the gradient explodes: updates become enormous and training destabilises.

    The knife-edge at $\lambda = 1$ is why plain RNNs are hard to train beyond ~10–20 steps.

    top singular value           T=1           T=5          T=10          T=25          T=50
    ------------------------------------------------------------------------------------
                   0.50     2.137e-01     1.616e-03     6.725e-06     8.085e-14     1.405e-26
                   0.90     3.847e-01     3.054e-02     2.401e-03     1.947e-07     8.154e-14
                   1.00     4.275e-01     5.172e-02     6.886e-03     2.713e-06     1.582e-11
                   1.11     4.745e-01     8.715e-02     1.955e-02     3.685e-05     2.920e-09
                   1.30     5.557e-01     1.920e-01     9.494e-02     1.914e-03     7.878e-06
    ------------------------------------------------------------------------------------
    
    our Wh has top singular value 2.3548
       after 6 steps  : ~1.7049e+02
       after 50 steps : ~3.9579e+18
       after 200 steps: ~2.4538e+74
    
    
    what 'vanishing' means in practice, in float64:
    
       scale 0.5: gradient reaches 1e-15 at step 29  -> numerically ZERO beyond that
       scale 0.9: gradient reaches 1e-15 at step 58  -> numerically ZERO beyond that
    
       Beyond that point the early timesteps receive no learning signal at
       all. The network is not learning slowly from them; it is not learning
       from them.
    Only the black line (exactly 1.0) is stable, and hitting it exactly is
    not something training can be relied on to do. Everything else either
    dies or blows up - geometrically.

    5. The LSTM — a Gradient Highway

    The LSTM's answer is not a bigger matrix. It is a second state, the cell state $\mathbf{c}_t$, whose update is additive rather than multiplicative:

    $$\mathbf{c}_t = \mathbf{f}_t \odot \mathbf{c}_{t-1} \;+\; \mathbf{i}_t \odot \tilde{\mathbf{c}}_t$$

    Compare the two recursions:

    $$\text{RNN: } \; \mathbf{h}_t = \tanh(W_h\mathbf{h}_{t-1} + \dots) \qquad\qquad \text{LSTM: } \; \mathbf{c}_t = \mathbf{f}_t\odot\mathbf{c}_{t-1} + \dots$$

    Going backwards, the RNN multiplies by $W_h$ every step. The LSTM multiplies by $\mathbf{f}_t$ — and if the forget gate is near 1, that is multiplying by one. The gradient can travel hundreds of steps essentially unchanged. That is the "constant error carousel".

    Three gates control the flow, each a sigmoid producing values in $(0,1)$:

    Gate Formula Question it answers
    forget $\mathbf{f}_t$ $\sigma(W_f[\mathbf{h}_{t-1},\mathbf{x}_t] + b_f)$ what should I discard?
    input $\mathbf{i}_t$ $\sigma(W_i[\dots] + b_i)$ what new is worth storing?
    output $\mathbf{o}_t$ $\sigma(W_o[\dots] + b_o)$ what should I expose now?

    with a candidate $\tilde{\mathbf{c}}_t = \tanh(W_c[\dots] + b_c)$ and finally $\mathbf{h}_t = \mathbf{o}_t \odot \tanh(\mathbf{c}_t)$.

    LSTM parameters: 4 gates x (5 x 3 + 3) = 72
    plain RNN had  : 18
    ratio          : 4.0x more
    
    the forget-gate bias is initialised to +1 on purpose: sigmoid(1) = 0.7311,
    so the gate starts nearly OPEN and the cell state is preserved by default.
    
    
    running the LSTM, one timestep at a time
    ==============================================================================
    
    --- t = 0 ---   x_t = [0.2 0.1]
       forget f_t    = [0.7067 0.754  0.7265]   (1 = keep, 0 = erase)
       input  i_t    = [0.4498 0.4688 0.491 ]   (how much of the candidate)
       candidate c~  = [ 0.1002 -0.1754  0.0629]
       output o_t    = [0.4947 0.4941 0.4983]
       c_t = f*c_prev + i*c~
           = [0.7067 0.754  0.7265] * [0. 0. 0.] + [0.4498 0.4688 0.491 ] * [ 0.1002 -0.1754  0.0629]
           = [0. 0. 0.] + [ 0.0451 -0.0823  0.0309] = [ 0.0451 -0.0823  0.0309]
       h_t = o_t * tanh(c_t) = [ 0.0223 -0.0406  0.0154]
    
       element 0 of the forget gate, in full:
          z = [h_prev, x_t] = [0.  0.  0.  0.2 0.1]
          (+0.0000)*(+0.0137) + (+0.0000)*(-0.2041) + (+0.0000)*(+0.2279) + 
          (+0.2000)*(-0.7389) + (+0.1000)*(+0.2722)
          + b = +1.0000  ->  total +0.879429
          sigmoid(+0.879429) = 0.706704   MATCH: True
    
    --- t = 1 ---   x_t = [0.25 0.15]
       forget f_t    = [0.7044 0.7622 0.7294]   (1 = keep, 0 = erase)
       input  i_t    = [0.4338 0.457  0.4909]   (how much of the candidate)
       candidate c~  = [ 0.1202 -0.2344  0.0633]
       output o_t    = [0.4974 0.4876 0.4955]
       c_t = f*c_prev + i*c~
           = [0.7044 0.7623 0.7294] * [ 0.0451 -0.0822  0.0309] + [0.4338 0.457  0.4909] * [ 0.1202 -0.2344  0.0633]
           = [ 0.0318 -0.0627  0.0225] + [ 0.0521 -0.1071  0.0311] = [ 0.0839 -0.1698  0.0536]
       h_t = o_t * tanh(c_t) = [ 0.0416 -0.082   0.0265]
    
    --- t = 2 ---   x_t = [0.3 0.2]
       forget f_t    = [0.7018 0.7701 0.7318]   (1 = keep, 0 = erase)
       input  i_t    = [0.4184 0.4448 0.4912]   (how much of the candidate)
       candidate c~  = [ 0.1394 -0.2906  0.0623]
       output o_t    = [0.4996 0.4807 0.493 ]
       c_t = f*c_prev + i*c~
           = [0.7018 0.7701 0.7318] * [ 0.0839 -0.1698  0.0536] + [0.4184 0.4448 0.4912] * [ 0.1394 -0.2906  0.0623]
           = [ 0.0589 -0.1308  0.0392] + [ 0.0583 -0.1293  0.0306] = [ 0.1172 -0.2601  0.0698]
       h_t = o_t * tanh(c_t) = [ 0.0583 -0.1223  0.0343]
    
    --- t = 3 ---   x_t = [0.9 0.8]
       forget f_t    = [0.6425 0.8272 0.7127]   (1 = keep, 0 = erase)
       input  i_t    = [0.2543 0.3144 0.4681]   (how much of the candidate)
       candidate c~  = [ 0.4644 -0.7782  0.2662]
       output o_t    = [0.5171 0.4226 0.468 ]
       c_t = f*c_prev + i*c~
           = [0.6425 0.8272 0.7127] * [ 0.1172 -0.2601  0.0698] + [0.2543 0.3144 0.468 ] * [ 0.4644 -0.7782  0.2662]
           = [ 0.0753 -0.2151  0.0497] + [ 0.1181 -0.2447  0.1246] = [ 0.1934 -0.4598  0.1743]
       h_t = o_t * tanh(c_t) = [ 0.0988 -0.1817  0.0808]
    
    --- t = 4 ---   x_t = [1.1  0.95]
       forget f_t    = [0.6231 0.847  0.7159]   (1 = keep, 0 = erase)
       input  i_t    = [0.2112 0.283  0.4605]   (how much of the candidate)
       candidate c~  = [ 0.5403 -0.8489  0.3089]
       output o_t    = [0.5212 0.4103 0.4609]
       c_t = f*c_prev + i*c~
           = [0.6231 0.847  0.7159] * [ 0.1934 -0.4598  0.1743] + [0.2112 0.283  0.4605] * [ 0.5403 -0.8489  0.3088]
           = [ 0.1205 -0.3894  0.1248] + [ 0.1141 -0.2402  0.1422] = [ 0.2346 -0.6297  0.2671]
       h_t = o_t * tanh(c_t) = [ 0.1201 -0.2289  0.1202]
    
    --- t = 5 ---   x_t = [1.2  1.05]
       forget f_t    = [0.6165 0.8562 0.7186]   (1 = keep, 0 = erase)
       input  i_t    = [0.1885 0.2647 0.4573]   (how much of the candidate)
       candidate c~  = [ 0.5803 -0.8781  0.3266]
       output o_t    = [0.5262 0.4015 0.4558]
       c_t = f*c_prev + i*c~
           = [0.6165 0.8562 0.7186] * [ 0.2346 -0.6297  0.267 ] + [0.1885 0.2647 0.4573] * [ 0.5803 -0.8781  0.3266]
           = [ 0.1446 -0.5391  0.1919] + [ 0.1094 -0.2324  0.1493] = [ 0.254  -0.7716  0.3412]
       h_t = o_t * tanh(c_t) = [ 0.1309 -0.2601  0.1498]
    
    ==============================================================================
    
    gate activity over time (mean across the 3 units)
    
      step     forget      input     output      ||c||      ||h||
    ------------------------------------------------------------
        t0    0.72907    0.46989    0.49572    0.09875    0.04876
        t1    0.73201    0.46058    0.49351    0.19686    0.09573
        t2    0.73458    0.45149    0.49109    0.29367    0.13973
        t3    0.72747    0.34560    0.46923    0.52842    0.22202
        t4    0.72865    0.31822    0.46411    0.72307    0.28505
        t5    0.73046    0.30350    0.46117    0.88106    0.32742
    ------------------------------------------------------------
    
    The forget gate stays near 0.73 -
    close to 1 - so the cell state is carried forward almost intact. That is
    the highway the gradient travels along.
    why the additive update saves the gradient
    
    going backwards, the cell state contributes a factor of f_t per step,
    not a matrix multiply:
    
      steps back     RNN: lambda^T     LSTM: f^T (f=0.95)     LSTM: f^T (f=0.99)
    ----------------------------------------------------------------------------
               1        2.3548e+00             9.5000e-01             9.9000e-01
               5        7.2401e+01             7.7378e-01             9.5099e-01
              10        5.2419e+03             5.9874e-01             9.0438e-01
              50        3.9579e+18             7.6945e-02             6.0501e-01
             100        1.5665e+37             5.9205e-03             3.6603e-01
             500       9.4321e+185             7.2745e-12             6.5705e-03
    ----------------------------------------------------------------------------
    
    At 500 steps the RNN factor is 9.4e+185 - numerically zero. With a
    forget gate at 0.99 the LSTM retains 0.007 of the signal.
    
    The gate is LEARNED, so the network can choose to remember (f near 1)
    or deliberately forget (f near 0) per unit and per timestep. A plain RNN
    has no such choice - Wh applies to everything, always.

    The GRU — the same idea, fewer parts

    The GRU merges the forget and input gates into a single update gate $z_t$ and drops the separate cell state:

    $$\mathbf{h}_t = (1 - \mathbf{z}_t)\odot\mathbf{h}_{t-1} \;+\; \mathbf{z}_t\odot\tilde{\mathbf{h}}_t$$

    The crucial property survives: $\mathbf{h}_{t-1}$ is carried by an interpolation, not a matrix multiply, so the gradient path is preserved. It has 3 gate-sets instead of 4, so about 25% fewer parameters, and in practice performs comparably.

    cell    gate sets    parameters (D_in=2, D_hid=3)
    ------------------------------------------------------
         RNN            1                              18
         GRU            3                              54
        LSTM            4                              72
    ------------------------------------------------------
    
    the carry term in each:
    
       RNN  : h_t = tanh(Wh @ h_prev + ...)        <- matrix multiply, dangerous
       LSTM : c_t = f * c_prev + i * c~            <- elementwise, safe
       GRU  : h_t = (1-z) * h_prev + z * h~        <- elementwise, safe
    
    Both gated cells replaced a matrix multiply with an elementwise gate.
    That single substitution is what made sequences of hundreds of steps
    trainable, and it is the same trick residual connections use in very
    deep feed-forward networks.

    6. Checking Against PyTorch

    PyTorch's nn.RNN and nn.LSTM use the same equations with one difference in bookkeeping: they keep two bias vectors (b_ih and b_hh) where the maths above uses one, and they store weights transposed. Matching them exactly is therefore a good test that the equations here are the real ones.

    hidden states, ours vs torch
    
      step                                ours                               torch
    --------------------------------------------------------------------------------
        t0           [ 0.2078 -0.322   0.0372]           [ 0.2078 -0.322   0.0372]
        t1           [-0.5683 -0.4711 -0.02  ]           [-0.5683 -0.4711 -0.02  ]
        t2           [ 0.052  -0.4551  0.4274]           [ 0.052  -0.4551  0.4274]
        t3           [-0.2097 -0.9506 -0.0788]           [-0.2097 -0.9506 -0.0788]
        t4           [-0.5402 -0.9649  0.4722]           [-0.5402 -0.9649  0.4722]
        t5           [-0.2328 -0.9827  0.3371]           [-0.2328 -0.9827  0.3371]
    --------------------------------------------------------------------------------
    
    MATCH: True
    max difference: 2.220e-16
    
    
    LSTM against torch
    
      step                             our h_t                           torch h_t
    --------------------------------------------------------------------------------
        t0           [ 0.0223 -0.0405  0.0154]           [ 0.0223 -0.0405  0.0154]
        t1           [ 0.0416 -0.082   0.0265]           [ 0.0416 -0.082   0.0265]
        t2           [ 0.0583 -0.1223  0.0344]           [ 0.0583 -0.1223  0.0344]
        t3           [ 0.0988 -0.1817  0.0808]           [ 0.0988 -0.1817  0.0808]
        t4           [ 0.1201 -0.2289  0.1202]           [ 0.1201 -0.2289  0.1202]
        t5           [ 0.1309 -0.2601  0.1498]           [ 0.1309 -0.2601  0.1498]
    --------------------------------------------------------------------------------
    
    hidden states MATCH: True
    final cell state MATCH: True
       ours  c_T = [ 0.254  -0.7716  0.3412]
       torch c_T = [ 0.254  -0.7716  0.3412]
    max difference: 1.110e-16
    
    The gate ordering (i, f, g, o) had to be matched exactly - getting it
    wrong is a classic source of silent bugs when porting LSTM weights.

    7. Summary

    Plain RNN LSTM GRU
    State $\mathbf{h}_t$ $\mathbf{h}_t$, $\mathbf{c}_t$ $\mathbf{h}_t$
    Carry $\tanh(W_h\mathbf{h}_{t-1}+\dots)$ $\mathbf{f}_t\odot\mathbf{c}_{t-1}+\dots$ $(1-\mathbf{z}_t)\odot\mathbf{h}_{t-1}+\dots$
    Gates none 3 2
    Parameter sets 1 4 3
    Long dependencies poor good good

    The one idea: carry information forward through time with the same weights, so sequence length stops mattering to the parameter count.

    The one flaw, and its fix: reusing $W_h$ at every step means the backward pass multiplies by it repeatedly, so gradients die or explode geometrically. Gated cells replace that matrix multiply with an elementwise gate near 1, turning multiplication by a matrix into multiplication by (almost) one. The same trick — an additive path that gradients can travel along untouched — reappears as residual connections in deep networks and as the residual stream in Transformers.

    Why Transformers displaced these: an RNN must process step $t$ before step $t+1$, so training cannot be parallelised across time. Attention looks at all positions at once, and gets a path length of 1 between any two positions instead of $O(T)$ — which is where the Autoregressive Transformer and BERT walkthroughs pick up.

    What is deliberately left out: bidirectional and multi-layer stacking, gradient clipping (the standard patch for the exploding case), truncated BPTT, sequence-to-sequence with attention, and teacher forcing.

    References

    1. Long Short-Term Memory โ€” Hochreiter & Schmidhuber, 1997 (Neural Computation)