Free demoJob-oriented courses at Debugshala, Old Palasia, Indore. Freshers, non-IT and year-gap welcome.Book a free demo +91-8982385539

RNN and LSTM Explained: Recurrent Neural Networks in NumPy

Lesson 7 of 9 · 11 min · Updated 13 Nov 2026

RNN and LSTM explained with NumPy cell steps: hidden state, backpropagation through time, vanishing gradients, LSTM gates, GRU and a PyTorch forecaster.

Ride an iBus along the BRTS corridor from Niranjanpur to Rajiv Gandhi Square and watch the conductor. At every stop she updates one number in her head: passengers on board. She does not recount the whole bus. She takes the previous count, adds the people who boarded and subtracts those who left.

Now the depot asks whether a ticket checker boarded forty stops ago. Her running count cannot say; it was overwritten long ago. So she keeps a small notebook, writes down only what matters and crosses out what no longer does.

The running count is the hidden state of a recurrent neural network. The notebook, with its rules for writing and erasing, is the cell state of an LSTM.

What is a recurrent neural network?

Map the bus to the model:

  • Each stop is one time step t in the sequence.
  • The boarding and leaving numbers are the input x_t.
  • The count in her head is the hidden state h_t.
  • Her rule "old count plus change" is the recurrent cell, the same rule at every stop.

A recurrent neural network is a neural network for sequential data that applies the same cell at each time step, combining the current input with a hidden state carried over from the previous step, so its output depends on everything it has seen so far.

The networks earlier in this series, from neural network from scratch to CNNs, take fixed-size inputs. A sentence or a sensor stream has variable length, and order matters. RNNs handle both.

The RNN cell step

One step computes h_t = tanh(Wx · x_t + Wh · h_(t−1) + b). Wx reads the new input, Wh reads the memory, and tanh keeps values between −1 and 1. Unrolling the loop over time gives the picture below.

Recurrent neural network unrolled through time: the same RNN cell with shared weights reads x1, x2 and x3 and passes the hidden state forward

An RNN unrolled over three time steps. It is one cell, drawn three times: the weights Wx and Wh are shared, and only the hidden state changes.

Python
import numpy as np
rng = np.random.default_rng(0)

input_size, hidden_size = 2, 4
Wx = rng.normal(scale=0.5, size=(hidden_size, input_size))   # input -> hidden
Wh = rng.normal(scale=0.5, size=(hidden_size, hidden_size))  # hidden -> hidden (memory)
b = np.zeros(hidden_size)

def rnn_step(x_t, h_prev):
    return np.tanh(Wx @ x_t + Wh @ h_prev + b)

# 5 bus stops: (people boarding, people leaving), scaled to 0-1
sequence = np.array([[0.8, 0.0], [0.5, 0.1], [0.0, 0.6], [0.3, 0.3], [0.0, 0.0]])
h = np.zeros(hidden_size)                                    # empty memory at the start
for t, x_t in enumerate(sequence, start=1):
    h = rnn_step(x_t, h)                                     # same weights every step
    print(f"step {t}: input {x_t}  ->  hidden {np.round(h, 3)}")
print("parameters:", Wx.size + Wh.size + b.size, "(same count for 5 steps or 5,000)")

The hidden state changes at every stop, even at step 5 where the input is zero, because memory alone drives it. The 28 parameters do not grow with sequence length: weight sharing over time, as a CNN shares over space.

How RNNs are trained: backpropagation through time

To train an RNN, you unroll it over the whole sequence and run ordinary backpropagation through the unrolled graph. This is called backpropagation through time (BPTT). The gradient for Wh is summed over every step. Long sequences are cut into chunks, which is truncated BPTT.

Quick check

Why does an RNN have the same number of parameters for a 10-step and a 1,000-step sequence?

Why do RNNs forget? The vanishing gradient through time

To learn that step 3 affects the output at step 50, the gradient must travel back 47 steps. At each step it is multiplied by the tanh slope and by Wh. If those factors are usually below 1, the gradient shrinks exponentially; if above 1, it explodes.

Python
import numpy as np
rng = np.random.default_rng(2)

H = 16
Wx = rng.normal(scale=0.5, size=(H, 1))
Wh = rng.normal(scale=0.9 / np.sqrt(H), size=(H, H))
xs = rng.normal(size=(60, 1))                      # a 60-step input sequence

h, J = np.zeros(H), np.eye(H)                      # J = d h_t / d h_0
for t, x_t in enumerate(xs, start=1):
    h = np.tanh(Wx @ x_t + Wh @ h)
    J = np.diag(1 - h ** 2) @ Wh @ J               # chain rule through one more step
    if t in (1, 5, 10, 20, 40, 60):
        print(f"gradient reaching {t:2d} steps back: {np.linalg.norm(J):.2e}")

After 20 steps the gradient has fallen by about 100 times, and after 60 steps by about 2 million. The network effectively cannot learn dependencies that far apart. This is the same multiplication problem described in activation functions, only stretched across time instead of layers.

Exploding gradients have a simple fix, gradient clipping: rescale the gradient when its norm passes a threshold such as 1.0. Vanishing gradients need a new architecture.

What is an LSTM and how does it remember?

An LSTM adds a second memory, the cell state c_t, which works like the conductor's notebook. Three gates, each a sigmoid layer producing values between 0 and 1, control it:

  1. The forget gate f decides how much of the old notebook to keep. 1 means keep everything, 0 means erase.
  2. The input gate i decides how much of the new candidate information g to write.
  3. The cell updates by addition: c_t = f · c_(t−1) + i · g.
  4. The output gate o decides how much of the notebook to reveal as the hidden state: h_t = o · tanh(c_t).

An LSTM is a recurrent neural network cell that keeps a separate cell state and uses learned forget, input and output gates to control what is erased, written and exposed at each step, which lets it learn dependencies across long sequences.

LSTM cell diagram showing the cell state highway, the forget gate, input gate with candidate values, and output gate producing the hidden state

Inside an LSTM cell. The cell state runs along the top like a highway, changed only by a multiply (forget) and an add (write), so information and gradients can pass many steps with little loss.

The key is step 3. Because the cell state is updated by addition, the gradient flowing back along it is multiplied only by the forget gate. When something is worth remembering, the network learns to push that gate close to 1.

Here is a full LSTM step in NumPy. The forget-gate bias is set to 3 to imitate a gate that has learned "keep this", and the loop tracks how much gradient survives along the cell path:

Python
import numpy as np
rng = np.random.default_rng(1)
sigmoid = lambda z: 1 / (1 + np.exp(-z))

H, D = 8, 1
W = {g: rng.normal(scale=0.3, size=(H, D + H)) for g in "figo"}
b = {"f": np.full(H, 3.0), "i": np.zeros(H), "g": np.zeros(H), "o": np.zeros(H)}

def lstm_step(x_t, h_prev, c_prev):
    z = np.concatenate([x_t, h_prev])               # input and previous output together
    f = sigmoid(W["f"] @ z + b["f"])                # forget gate: keep old memory?
    i = sigmoid(W["i"] @ z + b["i"])                # input gate: write new info?
    g = np.tanh(W["g"] @ z + b["g"])                # candidate info to write
    o = sigmoid(W["o"] @ z + b["o"])                # output gate: reveal memory?
    c = f * c_prev + i * g                          # cell state update (addition!)
    h = o * np.tanh(c)
    return h, c, f

h, c, keep = np.zeros(H), np.zeros(H), np.ones(H)   # keep = d c_t / d c_0 along the cell path
for t, x_t in enumerate(rng.normal(size=(60, 1)), start=1):
    h, c, f = lstm_step(x_t, h, c)
    keep *= f
    if t in (1, 5, 10, 20, 40, 60):
        print(f"step {t:2d}: forget gate mean {f.mean():.2f}, cell-path gradient {keep.mean():.2e}")

After 60 steps, about 6% of the gradient still survives along the cell path, compared with roughly two millionths in the plain RNN.

LSTM vs GRU vs plain RNN

Plain RNN LSTM GRU
Memory Hidden state only Hidden state + cell state Hidden state only
Gates None Forget, input, output Update, reset
Weight matrices per layer 1 set 4 sets 3 sets
Long-range memory Weak Strong Strong
Speed Fastest Slowest In between
Good for Very short sequences Long sequences, default choice When LSTM is too slow or data is small

The GRU (gated recurrent unit) merges the forget and input gates into one update gate and drops the separate cell state. It often matches an LSTM with fewer parameters.

Quick check

Which LSTM gate decides how much of the previous cell state to keep?

Building an LSTM in PyTorch

This PyTorch 2.x model reads a window of past values, for example 30 days of a shop's sales, and predicts the next day:

Python
import torch
from torch import nn

class SalesForecaster(nn.Module):
    def __init__(self, hidden_size=32):
        super().__init__()
        self.lstm = nn.LSTM(input_size=1, hidden_size=hidden_size,
                            num_layers=1, batch_first=True)
        self.head = nn.Linear(hidden_size, 1)

    def forward(self, x):                 # x: (batch, 30 days, 1 feature)
        out, (h_n, c_n) = self.lstm(x)    # out: (batch, 30, hidden_size)
        return self.head(out[:, -1, :])   # use the last step's hidden state

model = SalesForecaster()
pred = model(torch.randn(16, 30, 1))      # (16, 1)

# inside the training loop, after loss.backward():
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

Note batch_first=True: without it, PyTorch expects (sequence, batch, features). Swap nn.LSTM for nn.GRU and the forward pass returns out, h_n with no cell state. In Keras 3, keras.layers.LSTM(32) returns only the last hidden state by default; add return_sequences=True to get every step.

For forecasting work, compare any LSTM against simple baselines first, as covered in time series analysis basics. Sequence models are part of Debugshala's full Data Science & AI curriculum, while Debugshala's machine learning classes covering sequence models go deeper into training them.

RNNs and LSTMs vs transformers

RNNs read one step at a time, so training cannot run positions in parallel, and very long contexts still fade. Transformers use attention to let every position look at every other position at once, which is why large language models use them. The full story is in transformers and attention explained.

Common mistakes with RNNs and LSTMs

  • Mixing up tensor layouts. Check batch_first in PyTorch; a swapped batch and time axis trains without error but learns nothing useful.
  • Skipping gradient clipping. RNN gradients can explode suddenly and turn the loss into nan.
  • Unscaled inputs. Sales in rupees (thousands) next to percentages wrecks the tanh and sigmoid gates. Standardise each feature.
  • Leaking the future. Build validation windows strictly after the training period.

Key takeaways

  • An RNN updates a hidden state at every step with shared weights, so it handles any sequence length.
  • Backpropagation through time multiplies one factor per step, so plain RNN gradients vanish or explode.
  • An LSTM's additive cell state and learned gates keep both information and gradients alive.
  • GRUs are a lighter gated alternative; clip gradients for any recurrent model.
  • Transformers have replaced RNNs for large text models, but LSTMs still fit small and streaming tasks.

Next, autoencoders use networks like these to compress data without labels. To keep loops and state updates sharp, work through sequence and loop quizzes on Debugshala Test.

Take the free Deep Learning quiz10 quick questions with explanations. No sign-up needed, and you can come back to this lesson after.

FAQ

What is the difference between RNN and LSTM?

An RNN keeps one hidden state updated by a single tanh layer, so it forgets quickly. An LSTM adds a separate cell state and three gates that control forgetting, writing and output, which keeps gradients alive so it learns long-range patterns plain RNNs miss.

Is LSTM still used in 2026?

Yes, though less for language. LSTMs and GRUs are still used for time series forecasting, sensor streams, speech on small devices and problems with little data. For large text tasks, transformers are standard because they train in parallel and scale better.

What is the vanishing gradient problem in RNNs?

It is the shrinking of the training signal as it travels back through many time steps. Each step multiplies the gradient by factors usually below 1, so after dozens of steps early inputs stop influencing learning. LSTMs and GRUs were designed to fix this.

What is the hidden state in an RNN?

The hidden state is a vector summarising everything the network has seen so far in the sequence. Each step computes it from the new input and the previous hidden state, and it feeds any output layer.

Where can I learn RNNs and LSTMs in Indore?

Debugshala teaches neural networks, including sequence models such as RNNs and LSTMs, in its Machine Learning and Data Science & AI courses in Indore. Classes are held at the Old Palasia campus in English and Hindi, and Data Science & AI is also available online. A free demo class is offered first.

From the team behind this site

Go further with the Data Science & AI Course at Debugshala

These tutorials are written by Debugshala, a training and placement institute at Old Palasia, Indore. The classroom course adds live mentoring, real projects and interview preparation.

  • Offline classes at Old Palasia, Indore
  • Free demo class before you pay
  • Non-IT, fresher and year-gap friendly
  • Placement assistance

700+ students placed · 200+ hiring partners · 4.9★ Google rating

Convolutional Neural Networks (CNN) Explained with NumPyAutoencoders Explained: Encoder, Bottleneck and Anomaly Use