Skip to content

24. Recurrent Neural Networks

Understand how Recurrent Neural Networks (RNNs) model sequential data by maintaining information across time, how hidden states and recurrent connections work, why vanilla RNNs struggle with long-term dependencies, and how RNNs provide the foundation for LSTMs, GRUs, and modern sequence modeling architectures.


🎯 Learning Objectives

After completing this chapter, you will be able to:

  • Explain what sequential data is
  • Understand why sequential data requires specialized modeling
  • Explain the basic architecture of a Recurrent Neural Network
  • Understand recurrent connections and hidden states
  • Explain how an RNN processes sequences step by step
  • Understand sequence-to-one, one-to-many, and sequence-to-sequence architectures
  • Understand the mathematical formulation of an RNN
  • Explain unrolling through time
  • Understand Backpropagation Through Time (BPTT)
  • Explain the vanishing gradient problem
  • Explain the exploding gradient problem
  • Understand why vanilla RNNs struggle with long-term dependencies
  • Understand teacher forcing
  • Understand many-to-one and many-to-many prediction
  • Build RNN models using PyTorch
  • Understand nn.RNN
  • Prepare sequential datasets for RNN training
  • Handle variable-length sequences
  • Understand padding and masking
  • Understand bidirectional RNNs
  • Understand stacked RNNs
  • Apply RNNs to time-series and NLP problems
  • Understand the limitations of vanilla RNNs
  • Understand why LSTM and GRU architectures were introduced
  • Evaluate RNNs from a production perspective
  • Understand where RNNs fit in the evolution toward Transformers

πŸ“– Overview

Many Machine Learning problems involve data where order matters.

Examples include:

Time-Series Data
      ↓
Stock Prices
      ↓
Sensor Measurements
      ↓
Weather Observations
      ↓
Speech Signals
      ↓
Text
      ↓
User Activity Sequences

In these problems, the meaning of the current observation may depend on previous observations.

For example:

"I went to the bank to deposit..."

Understanding the next word depends on the sequence that came before it.

Similarly:

Temperature:
25 β†’ 26 β†’ 28 β†’ 31 β†’ 34

The current value can be interpreted better when the historical sequence is considered.

Traditional feed-forward neural networks treat inputs independently.

RNNs introduce a mechanism for carrying information from one time step to the next.


🧠 What is a Recurrent Neural Network?

A Recurrent Neural Network is a neural network architecture designed to process sequential data by maintaining a hidden state that carries information across time steps.

The fundamental idea is:

Current Input
      +
Previous Hidden State
      ↓
RNN Cell
      ↓
Current Hidden State

The hidden state acts as a form of learned memory.


🧠 Feed-Forward Network vs RNN

Feed-Forward Network

Input
  ↓
Layer
  ↓
Layer
  ↓
Output

The network does not naturally maintain information about previous inputs.


Recurrent Neural Network

x₁ ──► RNN ──► h₁
         β”‚
         β–Ό
xβ‚‚ ──► RNN ──► hβ‚‚
         β”‚
         β–Ό
x₃ ──► RNN ──► h₃
         β”‚
         β–Ό
xβ‚„ ──► RNN ──► hβ‚„

Each time step receives:

Current Input
+
Previous Hidden State

🧠 Core RNN Idea

The central concept is:

The current hidden state depends on both the current input and the previous hidden state.

Conceptually:

[ h_t=f(x_t,h_{t-1}) ]

where:

xβ‚œ     = Current Input
hβ‚œβ‚‹β‚   = Previous Hidden State
hβ‚œ     = Current Hidden State

🧠 RNN Architecture

flowchart LR

    X1["x₁"] --> R1["RNN Cell"]
    R1 --> H1["h₁"]

    X2["xβ‚‚"] --> R2["RNN Cell"]
    H1 --> R2
    R2 --> H2["hβ‚‚"]

    X3["x₃"] --> R3["RNN Cell"]
    H2 --> R3
    R3 --> H3["h₃"]

    X4["xβ‚„"] --> R4["RNN Cell"]
    H3 --> R4
    R4 --> H4["hβ‚„"]

The same RNN cell parameters are reused at every time step.


🧠 RNN Memory

An RNN does not store the entire historical sequence explicitly.

Instead, it maintains a learned representation:

hβ‚œ

which summarizes information from previous time steps.

Conceptually:

x₁
 ↓
h₁
 ↓
xβ‚‚
 ↓
hβ‚‚
 ↓
x₃
 ↓
h₃
 ↓
xβ‚„
 ↓
hβ‚„

Therefore:

hβ‚„

contains information derived from:

x₁, xβ‚‚, x₃, xβ‚„

although older information may become difficult to preserve in a vanilla RNN.


🧠 Unrolling an RNN

An RNN cell can be represented as a single reusable component:

        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
xβ‚œ ───► β”‚   RNN Cell    β”‚ ───► hβ‚œ
hβ‚œβ‚‹β‚ ─►│               β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

When processing a sequence, the cell is conceptually unrolled:

       β”Œβ”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”
x₁ ──► β”‚ RNN β”‚ ────► β”‚ RNN β”‚ ────► β”‚ RNN β”‚
       β””β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”˜
          β”‚             β”‚             β”‚
          β–Ό             β–Ό             β–Ό
         h₁            hβ‚‚            h₃

The parameters are shared across all time steps.


🧠 RNN Unrolling

flowchart LR

    X1["x₁"]
    X2["xβ‚‚"]
    X3["x₃"]
    X4["xβ‚„"]

    R1["RNN"]
    R2["RNN"]
    R3["RNN"]
    R4["RNN"]

    H1["h₁"]
    H2["hβ‚‚"]
    H3["h₃"]
    H4["hβ‚„"]

    X1 --> R1
    R1 --> H1
    H1 --> R2

    X2 --> R2
    R2 --> H2
    H2 --> R3

    X3 --> R3
    R3 --> H3
    H3 --> R4

    X4 --> R4
    R4 --> H4

🧠 RNN Mathematical Formulation

A simple RNN hidden-state equation is:

[ h_t=\tanh(W_{xh}x_t+W_{hh}h_{t-1}+b_h) ]

where:

xβ‚œ       = Input at time t
hβ‚œβ‚‹β‚     = Previous hidden state
Wβ‚“β‚•      = Input-to-hidden weights
Wβ‚•β‚•      = Hidden-to-hidden weights
bβ‚•       = Hidden bias
tanh     = Activation function
hβ‚œ       = Current hidden state

🧠 Output Equation

The hidden state can be transformed into an output:

[ y_t=W_{hy}h_t+b_y ]

Therefore:

Input
 ↓
Hidden State
 ↓
Output

🧠 Complete RNN Computation

xβ‚œ
 β”‚
 β–Ό
Wβ‚“β‚•xβ‚œ
 β”‚
 β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β”‚              β”‚
 β–Ό              β–Ό
             Wβ‚•β‚•hβ‚œβ‚‹β‚
 β”‚              β”‚
 β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
        β–Ό
      Add
        β”‚
        β–Ό
      tanh
        β”‚
        β–Ό
       hβ‚œ
        β”‚
        β–Ό
     Wβ‚•α΅§hβ‚œ
        β”‚
        β–Ό
       yβ‚œ

🧠 Why Are Weights Shared?

The same RNN parameters are applied at every time step.

Time 1 β†’ W
Time 2 β†’ W
Time 3 β†’ W
Time 4 β†’ W

Not:

Time 1 β†’ W₁
Time 2 β†’ Wβ‚‚
Time 3 β†’ W₃
Time 4 β†’ Wβ‚„

This provides:

Parameter Sharing
+
Sequence-Length Flexibility
+
Reduced Number of Parameters

🧠 RNN Sequence Processing

Suppose the sequence is:

"I"
"love"
"machine"
"learning"

The RNN processes:

x₁ = "I"
 ↓
h₁

xβ‚‚ = "love"
 ↓
hβ‚‚

x₃ = "machine"
 ↓
h₃

xβ‚„ = "learning"
 ↓
hβ‚„

The state evolves as:

h₁ β†’ hβ‚‚ β†’ h₃ β†’ hβ‚„

🧠 RNN Input Representation

RNNs do not normally consume raw text directly.

A typical NLP pipeline is:

Text
 ↓
Tokenization
 ↓
Token IDs
 ↓
Embedding
 ↓
RNN
 ↓
Output

🧠 Embeddings + RNN

flowchart LR

    TEXT["Text"]

    TOKEN["Tokenization"]

    IDS["Token IDs"]

    EMBED["Embedding Layer"]

    RNN["RNN"]

    OUTPUT["Output"]

    TEXT --> TOKEN
    TOKEN --> IDS
    IDS --> EMBED
    EMBED --> RNN
    RNN --> OUTPUT

🧠 RNN Input Tensor

For batch-based training, an RNN commonly receives:

Batch
Sequence Length
Features

Conceptually:

(batch_size, sequence_length, input_size)

For example:

(32, 20, 128)

means:

32 sequences
20 time steps
128 features per time step

🧠 PyTorch RNN Input Shape

With:

batch_first=True

the expected input shape is:

(batch_size, sequence_length, input_size)

Without:

batch_first=True

PyTorch commonly expects:

(sequence_length, batch_size, input_size)

This distinction is important when building RNN pipelines.


🧠 Sequence-to-Sequence Mapping

RNN architectures can support different input/output patterns.

Common patterns include:

One-to-One
One-to-Many
Many-to-One
Many-to-Many

🧠 One-to-One

A standard classification model:

Input
 ↓
Model
 ↓
Output

Example:

Image Classification

RNNs are generally not needed for this pattern.


🧠 One-to-Many

One input produces a sequence.

Input
 ↓
RNN
 ↓
Output₁
 ↓
Outputβ‚‚
 ↓
Output₃

Example:

Image
 ↓
Caption

🧠 Many-to-One

A sequence produces one output.

x₁
 ↓
xβ‚‚
 ↓
x₃
 ↓
xβ‚„
 ↓
RNN
 ↓
Prediction

Examples:

Sentiment Classification
Sequence Classification
Activity Recognition

🧠 Many-to-Many

A sequence produces another sequence.

x₁ β†’ y₁
xβ‚‚ β†’ yβ‚‚
x₃ β†’ y₃
xβ‚„ β†’ yβ‚„

Examples:

Named Entity Recognition
Part-of-Speech Tagging
Sequence Labeling

🧠 Sequence Mapping Patterns

flowchart TD

    ONE_ONE["One-to-One<br>Input β†’ Output"]

    ONE_MANY["One-to-Many<br>Input β†’ Sequence"]

    MANY_ONE["Many-to-One<br>Sequence β†’ Output"]

    MANY_MANY["Many-to-Many<br>Sequence β†’ Sequence"]

🧠 Many-to-One Classification

For sentiment classification:

"I really enjoyed this movie"

Token 1
   ↓
Token 2
   ↓
Token 3
   ↓
Token 4
   ↓
Token 5
   ↓
Final Hidden State
   ↓
Classifier
   ↓
Positive

The final hidden representation is used for classification.


🧠 Many-to-One Architecture

flowchart LR

    X1["x₁"]
    X2["xβ‚‚"]
    X3["x₃"]
    X4["xβ‚„"]

    R1["RNN"]
    R2["RNN"]
    R3["RNN"]
    R4["RNN"]

    H1["h₁"]
    H2["hβ‚‚"]
    H3["h₃"]
    H4["hβ‚„"]

    CLASS["Classifier"]

    X1 --> R1
    R1 --> H1
    H1 --> R2

    X2 --> R2
    R2 --> H2
    H2 --> R3

    X3 --> R3
    R3 --> H3
    H3 --> R4

    X4 --> R4
    R4 --> H4

    H4 --> CLASS

🧠 Many-to-Many Sequence Labeling

For Named Entity Recognition:

John   lives   in   Berlin

 ↓       ↓      ↓      ↓

B-PER   O      O    B-LOC

The model produces an output for each time step.


🧠 Bidirectional RNN

A standard RNN processes:

Past β†’ Future

A Bidirectional RNN processes the sequence in both directions:

Forward:
x₁ β†’ xβ‚‚ β†’ x₃ β†’ xβ‚„

Backward:
xβ‚„ β†’ x₃ β†’ xβ‚‚ β†’ x₁

The two representations are combined.


🧠 Bidirectional RNN

flowchart LR

    X1["x₁"]
    X2["xβ‚‚"]
    X3["x₃"]
    X4["xβ‚„"]

    F["Forward RNN"]

    B["Backward RNN"]

    OUTPUT["Combined Representation"]

    X1 --> F
    X2 --> F
    X3 --> F
    X4 --> F

    X4 --> B
    X3 --> B
    X2 --> B
    X1 --> B

    F --> OUTPUT
    B --> OUTPUT

🧠 Why Use Bidirectional RNNs?

Some tasks benefit from both:

Previous Context
+
Future Context

For example:

"The bank approved the loan"

The meaning of a word may depend on information appearing later in the sentence.

Bidirectional RNNs can therefore be useful for:

Sequence Classification
Named Entity Recognition
Speech Processing
Sequence Labeling

🧠 Limitation of Bidirectional RNNs

Bidirectional models require access to the complete sequence.

Therefore they are generally unsuitable for strictly causal real-time prediction where future observations are unavailable.

For example:

Real-Time Streaming

Current Event
     ↓
Prediction

cannot use:

Future Events

that have not happened yet.


🧠 Stacked RNN

Multiple RNN layers can be stacked.

Input
 ↓
RNN Layer 1
 ↓
RNN Layer 2
 ↓
RNN Layer 3
 ↓
Output

The first layer learns lower-level temporal representations.

Higher layers can learn more abstract sequence patterns.


🧠 Stacked RNN Architecture

flowchart TD

    INPUT["Input Sequence"]

    R1["RNN Layer 1"]

    R2["RNN Layer 2"]

    R3["RNN Layer 3"]

    OUTPUT["Output"]

    INPUT --> R1
    R1 --> R2
    R2 --> R3
    R3 --> OUTPUT

🧠 Deep RNN

A stacked RNN creates depth in two dimensions:

Time

t₁ β†’ tβ‚‚ β†’ t₃ β†’ tβ‚„

and:

Layer 1
   ↓
Layer 2
   ↓
Layer 3

Conceptually:

          Time
      β†’     β†’     β†’     β†’

L1   h₁ β†’  hβ‚‚ β†’  h₃ β†’  hβ‚„
      ↓     ↓     ↓     ↓
L2   h₁ β†’  hβ‚‚ β†’  h₃ β†’  hβ‚„
      ↓     ↓     ↓     ↓
L3   h₁ β†’  hβ‚‚ β†’  h₃ β†’  hβ‚„

🧠 The Long-Term Dependency Problem

Consider:

"I grew up in France and I speak fluent ______."

The model may need to remember:

France

for many time steps before predicting:

French

A vanilla RNN can struggle to preserve this information over long sequences.

This is called the:

Long-Term Dependency Problem


⚠ Vanishing Gradient Problem

During training, gradients are propagated backward through time.

For a long sequence:

t₁
 ↓
tβ‚‚
 ↓
t₃
 ↓
...
 ↓
t₁₀₀

the gradient repeatedly passes through recurrent transformations.

If the gradients become smaller at each step:

1.0
 ↓
0.5
 ↓
0.25
 ↓
0.125
 ↓
...
 ↓
β‰ˆ 0

the network receives almost no useful gradient for earlier time steps.


🧠 Vanishing Gradients

Conceptually:

Loss
 ↓
Gradient
 ↓
t₁₀
 ↓
t₉
 ↓
tβ‚ˆ
 ↓
...
 ↓
t₁

Gradient magnitude
     ↓
     ↓
     ↓
     ↓
   β‰ˆ 0

This makes it difficult to learn long-range dependencies.


🧠 Mathematical Intuition

During recurrent backpropagation, gradients involve repeated multiplication of Jacobian terms.

Conceptually:

[ \frac{\partial L}{\partial h_t} \propto \prod_{k=t+1}^{T} \frac{\partial h_k}{\partial h_{k-1}} ]

If these factors tend to have magnitude below 1, repeated multiplication can cause the gradient to shrink rapidly.


⚠ Exploding Gradient Problem

The opposite can also occur.

If gradients repeatedly grow:

1
 ↓
2
 ↓
4
 ↓
8
 ↓
16
 ↓
...

the gradient can become extremely large.

This is called:

Exploding Gradients


🧠 Vanishing vs Exploding Gradients

Problem Gradient Behavior Effect
Vanishing Gradient Becomes very small Earlier time steps learn poorly
Exploding Gradient Becomes extremely large Training becomes unstable

🧠 Gradient Clipping

Gradient clipping can help control exploding gradients.

For example:

torch.nn.utils.clip_grad_norm_(
    model.parameters(),
    max_norm=1.0
)

Typical training flow:

loss.backward()

torch.nn.utils.clip_grad_norm_(
    model.parameters(),
    max_norm=1.0
)

optimizer.step()

This does not solve the fundamental long-term dependency problem, but it can stabilize training when gradients become excessively large.


🧠 Why Vanilla RNNs Struggle

The fundamental architecture repeatedly applies the same recurrent transformation.

h₁
 ↓
hβ‚‚
 ↓
h₃
 ↓
hβ‚„
 ↓
...
 ↓
h₁₀₀

Long sequences therefore create a long chain of dependencies.

The network may struggle to preserve important information from early time steps.


🧠 RNN β†’ LSTM β†’ GRU

The limitations of vanilla RNNs motivated improved recurrent architectures.

Vanilla RNN
     ↓
LSTM
     ↓
GRU

LSTM introduces:

Cell State
+
Gates

GRU provides a simpler gated mechanism.

The next chapter covers:

25. LSTM and GRU.


🧠 Teacher Forcing

In sequence generation, the model may predict one token at a time.

During training, instead of feeding the model's previous prediction back into the next step, the actual previous target can be provided.

This is called:

Teacher Forcing


🧠 Teacher Forcing

Without teacher forcing:

Prediction₁
    ↓
Inputβ‚‚
    ↓
Predictionβ‚‚
    ↓
Input₃

With teacher forcing:

Prediction₁

Actual Target₁
     ↓
   Inputβ‚‚
     ↓
Predictionβ‚‚

Actual Targetβ‚‚
     ↓
   Input₃

🧠 Teacher Forcing Trade-Off

Teacher forcing can make training faster and easier.

However, during inference:

Actual Previous Token

may not be available.

The model must use:

Its Own Previous Prediction

This creates a difference between:

Training
vs
Inference

known as:

Exposure Bias

🧠 RNN for Time-Series

RNNs can process time-series data.

Example:

Temperature
 ↓
Humidity
 ↓
Pressure
 ↓
Wind
 ↓
Future Temperature

A sliding sequence can be created:

[t₁, tβ‚‚, t₃] β†’ tβ‚„
[tβ‚‚, t₃, tβ‚„] β†’ tβ‚…
[t₃, tβ‚„, tβ‚…] β†’ t₆

🧠 Time-Series RNN Architecture

flowchart LR

    T1["t₁"]
    T2["tβ‚‚"]
    T3["t₃"]
    T4["tβ‚„"]

    R1["RNN"]
    R2["RNN"]
    R3["RNN"]

    H1["h₁"]
    H2["hβ‚‚"]
    H3["h₃"]

    PRED["Future Prediction"]

    T1 --> R1
    R1 --> H1
    H1 --> R2

    T2 --> R2
    R2 --> H2
    H2 --> R3

    T3 --> R3
    R3 --> H3

    H3 --> PRED

🐍 Part I β€” PyTorch RNN

PyTorch provides:

nn.RNN

for implementing vanilla recurrent networks.


πŸ§ͺ Create an RNN

import torch
import torch.nn as nn


rnn = nn.RNN(
    input_size=128,
    hidden_size=64,
    num_layers=1,
    batch_first=True
)

Here:

input_size  = 128
hidden_size = 64
num_layers  = 1

🧠 RNN Input Shape

With:

batch_first=True

the input shape is:

(batch_size, sequence_length, input_size)

Example:

x = torch.randn(
    32,
    20,
    128
)

means:

Batch Size      = 32
Sequence Length = 20
Features        = 128

πŸ§ͺ Forward Pass

output, hidden = rnn(
    x
)

The outputs represent the hidden representation for each time step.

Conceptually:

output
 ↓
h₁, hβ‚‚, h₃, ..., hβ‚œ

The final hidden state is also returned.


🧠 Output vs Hidden State

For a typical RNN:

output
=
Hidden State at Every Time Step

while:

hidden
=
Final Hidden State

For example:

output:

h₁
hβ‚‚
h₃
hβ‚„


hidden:

hβ‚„

🧠 Tensor Shapes

For:

num_layers = L
batch_size = B
sequence_length = T
hidden_size = H

with:

batch_first=True

the output shape is:

B Γ— T Γ— H

The hidden state shape is:

L Γ— B Γ— H

πŸ§ͺ Complete RNN Classifier

class RNNClassifier(
    nn.Module
):

    def __init__(
        self,
        input_size,
        hidden_size,
        num_classes
    ):

        super().__init__()

        self.rnn = nn.RNN(
            input_size=input_size,
            hidden_size=hidden_size,
            batch_first=True
        )

        self.fc = nn.Linear(
            hidden_size,
            num_classes
        )

    def forward(
        self,
        x
    ):

        output, hidden = self.rnn(
            x
        )

        last_hidden = hidden[-1]

        logits = self.fc(
            last_hidden
        )

        return logits

🧠 RNN Classifier Architecture

flowchart LR

    INPUT["Sequence Input"]

    RNN["RNN"]

    H["Final Hidden State"]

    FC["Linear Layer"]

    OUTPUT["Class Logits"]

    INPUT --> RNN
    RNN --> H
    H --> FC
    FC --> OUTPUT

πŸ§ͺ Create the Model

model = RNNClassifier(
    input_size=128,
    hidden_size=64,
    num_classes=3
)

πŸ§ͺ Loss and Optimizer

criterion = nn.CrossEntropyLoss()

optimizer = torch.optim.AdamW(
    model.parameters(),
    lr=1e-3,
    weight_decay=1e-4
)

πŸ§ͺ Training Loop

for epoch in range(
    epochs
):

    model.train()

    for x, y in train_loader:

        x = x.to(
            device
        )

        y = y.to(
            device
        )

        optimizer.zero_grad()

        logits = model(
            x
        )

        loss = criterion(
            logits,
            y
        )

        loss.backward()

        torch.nn.utils.clip_grad_norm_(
            model.parameters(),
            max_norm=1.0
        )

        optimizer.step()

🧠 Why Clip Gradients?

RNNs can suffer from exploding gradients.

Therefore:

Backward Pass
     ↓
Gradient Computation
     ↓
Gradient Clipping
     ↓
Optimizer Update

can improve training stability.


🧠 Multiple RNN Layers

PyTorch supports stacked RNNs:

rnn = nn.RNN(
    input_size=128,
    hidden_size=64,
    num_layers=3,
    batch_first=True
)

This creates:

RNN Layer 1
     ↓
RNN Layer 2
     ↓
RNN Layer 3

🧠 Dropout in Stacked RNNs

PyTorch supports dropout between recurrent layers when multiple layers are used.

For example:

rnn = nn.RNN(
    input_size=128,
    hidden_size=64,
    num_layers=3,
    dropout=0.2,
    batch_first=True
)

The exact dropout behavior depends on the framework implementation.


🧠 Bidirectional RNN in PyTorch

A bidirectional RNN can be created using:

rnn = nn.RNN(
    input_size=128,
    hidden_size=64,
    batch_first=True,
    bidirectional=True
)

The output hidden dimension becomes:

64 Γ— 2
=
128

because:

Forward Hidden State
+
Backward Hidden State

are combined.


🧠 Bidirectional Tensor Shape

For:

hidden_size = H
bidirectional = True

the output feature dimension becomes:

[ 2H ]

For example:

hidden_size = 64

Output Features = 128

πŸ§ͺ Bidirectional Classifier

class BiRNNClassifier(
    nn.Module
):

    def __init__(
        self,
        input_size,
        hidden_size,
        num_classes
    ):

        super().__init__()

        self.rnn = nn.RNN(
            input_size=input_size,
            hidden_size=hidden_size,
            batch_first=True,
            bidirectional=True
        )

        self.fc = nn.Linear(
            hidden_size * 2,
            num_classes
        )

    def forward(
        self,
        x
    ):

        output, hidden = self.rnn(
            x
        )

        forward_hidden = hidden[-2]

        backward_hidden = hidden[-1]

        combined = torch.cat(
            (
                forward_hidden,
                backward_hidden
            ),
            dim=1
        )

        return self.fc(
            combined
        )

🧠 Variable-Length Sequences

Real-world sequence data often has different lengths.

Example:

Sequence A β†’ 10 tokens
Sequence B β†’ 20 tokens
Sequence C β†’ 15 tokens

Batches require tensors with compatible dimensions.

A common solution is:

Padding
+
Packed Sequences

🧠 Padding

Sequences can be padded:

A:
[1, 2, 3, 4, PAD, PAD]

B:
[1, 2, 3, 4, 5, 6]

However, the model should avoid treating:

PAD

as meaningful input.


🧠 Packed Sequences

PyTorch provides utilities such as:

pack_padded_sequence

and:

pad_packed_sequence

to efficiently process variable-length sequences.


πŸ§ͺ Packed Sequence Example

from torch.nn.utils.rnn import (
    pack_padded_sequence,
    pad_packed_sequence
)


packed = pack_padded_sequence(
    x,
    lengths,
    batch_first=True,
    enforce_sorted=False
)


output, hidden = rnn(
    packed
)


output, lengths = (
    pad_packed_sequence(
        output,
        batch_first=True
    )
)

This allows the RNN to avoid unnecessary computation over padded positions.


🧠 Masking

Another common strategy is masking.

Conceptually:

Real Token β†’ 1
Padding    β†’ 0

The mask tells downstream operations which positions should contribute.

Masking becomes especially important for:

Attention
Loss Computation
Sequence Pooling
Evaluation

🧠 RNN Applications

RNNs have historically been used for:

Natural Language Processing

Text Classification
Language Modeling
Sequence Labeling
Named Entity Recognition
Machine Translation

Speech

Speech Recognition
Audio Sequence Modeling

Time-Series

Demand Forecasting
Sensor Prediction
Anomaly Detection
Financial Time Series

User Behavior

Clickstream Modeling
Session Prediction
Recommendation

🧠 RNN Application Architecture

flowchart TD

    DATA["Sequential Data"]

    PREP["Preprocessing"]

    EMBED["Feature / Embedding Representation"]

    RNN["RNN"]

    HEAD["Task Head"]

    OUTPUT["Prediction"]

    DATA --> PREP
    PREP --> EMBED
    EMBED --> RNN
    RNN --> HEAD
    HEAD --> OUTPUT

🧠 RNN for Language Modeling

A language model predicts the next token.

For:

"The weather is"

the model predicts:

"good"

Conceptually:

"The"
 ↓
"weather"
 ↓
"is"
 ↓
Prediction

🧠 Language Modeling

flowchart LR

    T1["The"]

    T2["weather"]

    T3["is"]

    R1["RNN"]
    R2["RNN"]
    R3["RNN"]

    P["Next Token"]

    T1 --> R1
    R1 --> R2

    T2 --> R2
    R2 --> R3

    T3 --> R3
    R3 --> P

🧠 RNN for Time-Series Forecasting

For a sequence:

[t₁, tβ‚‚, t₃, tβ‚„]

the model can predict:

tβ‚…

Then for rolling forecasting:

[tβ‚‚, t₃, tβ‚„, tβ‚…]

can be used to predict:

t₆

🧠 RNN Forecasting

Historical Sequence
        ↓
      RNN
        ↓
Next Value
        ↓
Updated Sequence
        ↓
      RNN
        ↓
Next Value

This can be used for multi-step forecasting, although recursive forecasting can accumulate prediction errors.


⚠ RNN Limitations

Vanilla RNNs have several limitations:

  • Vanishing gradients
  • Exploding gradients
  • Difficulty learning long-term dependencies
  • Sequential computation
  • Limited parallelism across time steps
  • Training can become slow for long sequences
  • Performance can degrade for very long sequences
  • Hidden-state bottleneck
  • Difficulty retaining information over long contexts

🧠 Sequential Computation Bottleneck

An RNN naturally processes:

x₁
 ↓
xβ‚‚
 ↓
x₃
 ↓
xβ‚„

The next computation depends on the previous hidden state.

Therefore, time steps cannot be fully parallelized during the recurrent computation.

This is an important difference from Transformer architectures.


🧠 RNN vs Transformer

RNN Transformer
Sequential processing Highly parallelizable during training
Hidden state Token representations + attention
Limited long-term memory Strong long-range relationship modeling
Recurrent computation Self-attention
Naturally handles sequential order Requires positional information
Older sequence architecture Modern dominant architecture for many sequence tasks

Transformers are covered in:

27. Transformer Architecture.


🧠 Evolution of Sequence Models

Feed-Forward Networks
        ↓
Vanilla RNN
        ↓
LSTM / GRU
        ↓
Attention
        ↓
Transformers
        ↓
Large Language Models
        ↓
Foundation Models

🧠 RNN β†’ LSTM β†’ Transformer

flowchart LR

    RNN["Vanilla RNN"]

    LSTM["LSTM / GRU"]

    ATTENTION["Attention"]

    TRANSFORMER["Transformer"]

    LLM["Large Language Models"]

    RNN --> LSTM
    LSTM --> ATTENTION
    ATTENTION --> TRANSFORMER
    TRANSFORMER --> LLM

RNNs remain important because they explain the historical and conceptual foundation of modern sequence modeling.


🏒 Enterprise Perspective

RNNs are less dominant than they once were for many large-scale sequence problems, but they remain useful for:

Streaming Data
Time-Series
Resource-Constrained Inference
Legacy ML Systems
Low-Latency Sequential Processing

They are also important for understanding why modern architectures evolved.


🏒 Production RNN Architecture

A production sequence-processing system may look like:

Data Source
    ↓
Streaming / Batch Pipeline
    ↓
Feature Engineering
    ↓
Sequence Builder
    ↓
RNN Model
    ↓
Prediction
    ↓
Business Service

🏒 Enterprise RNN Architecture

flowchart TD

    SOURCE["Event / Sensor / Text Data"]

    PIPELINE["Data Pipeline"]

    FEATURES["Feature Processing"]

    SEQUENCE["Sequence Builder"]

    MODEL["RNN Model"]

    PRED["Prediction"]

    BUSINESS["Business Service"]

    MONITOR["Monitoring"]

    SOURCE --> PIPELINE
    PIPELINE --> FEATURES
    FEATURES --> SEQUENCE
    SEQUENCE --> MODEL
    MODEL --> PRED
    PRED --> BUSINESS

    MODEL --> MONITOR
    PRED --> MONITOR

🏒 Streaming RNN Systems

RNNs can be attractive for streaming use cases because the hidden state can represent historical information.

Conceptually:

Event₁
 ↓
h₁

Eventβ‚‚
 ↓
hβ‚‚

Event₃
 ↓
h₃

Eventβ‚„
 ↓
hβ‚„

The state can be updated incrementally.

However, state management becomes an important production concern.


🏒 Stateful Inference

A stateful service might maintain:

User Session
      ↓
Hidden State
      ↓
New Event
      ↓
Updated Hidden State

But production systems must carefully manage:

Session Identity
State Expiration
Concurrency
Fault Recovery
State Persistence
Model Version Compatibility

🏒 RNN State Management

flowchart LR

    EVENT["Incoming Event"]

    SESSION["Session / Sequence"]

    STATE["Hidden State"]

    MODEL["RNN"]

    NEWSTATE["Updated Hidden State"]

    PRED["Prediction"]

    EVENT --> SESSION
    SESSION --> MODEL
    STATE --> MODEL
    MODEL --> NEWSTATE
    MODEL --> PRED
    NEWSTATE --> STATE

🏒 Model Versioning

For production sequence models, track:

Model Version
Training Dataset Version
Feature Version
Sequence Length
Tokenizer Version
Embedding Version
Training Configuration
Evaluation Metrics
Deployment Version

For NLP systems, also track:

Vocabulary
Tokenization Rules
Special Tokens
Padding Strategy

🏒 Monitoring RNN Systems

Production monitoring should include:

Infrastructure

CPU
GPU
Memory
Latency
Throughput

Model

Prediction Distribution
Accuracy
Precision
Recall
F1
Loss

Data

Input Drift
Feature Drift
Sequence Length Distribution
Missing Values
Padding Ratio

Operational State

Hidden-State Errors
Session State
State Expiration
Sequence Corruption

🏒 RNN Production Challenges

A production RNN may face:

Long Sequences
      ↓
Memory Growth
      ↓
Training Cost

and:

Stateful Inference
      ↓
State Management
      ↓
Operational Complexity

Therefore architecture decisions should consider whether an RNN is genuinely appropriate for the workload.


🧠 When Should You Use an RNN?

RNNs may still be reasonable when:

Sequence Length is Moderate
+
Streaming Processing is Important
+
Model is Relatively Small
+
Sequential State is Useful
+
Infrastructure is Constrained

🧠 When Should You Prefer LSTM / GRU?

Use gated recurrent architectures when:

Longer Dependencies
+
Sequential Processing
+
More Stable Memory

are important.

LSTM and GRU are covered in:

25. LSTM and GRU.


🧠 When Should You Prefer Transformers?

Transformers may be preferable when:

Long Context
+
Large-Scale Training
+
High Parallelism
+
Global Relationships
+
Large Pretrained Models

are important.


🧠 Sequence Architecture Decision

flowchart TD

    START["Sequential ML Problem"]

    STREAM["Streaming / Stateful Requirement"]

    LONG["Long-Term Dependencies"]

    SCALE["Large Data / Compute"]

    RNN["Vanilla RNN"]

    LSTM["LSTM / GRU"]

    TRANSFORMER["Transformer"]

    START --> STREAM

    STREAM -->|Strong| RNN
    STREAM -->|Not Critical| LONG

    LONG -->|Moderate| LSTM
    LONG -->|Very Long / Large Scale| SCALE

    SCALE -->|Large Data + Compute| TRANSFORMER

This is a conceptual decision guide rather than a universal rule.


πŸ§ͺ Practical Exercise 1 β€” Build a Vanilla RNN

Create:

Input Size = 32
Hidden Size = 64
Sequence Length = 20

Build a PyTorch RNN and inspect:

Output Shape
Hidden State Shape

πŸ§ͺ Practical Exercise 2 β€” Many-to-One Classification

Build an RNN classifier for:

3 Classes

Use:

Final Hidden State
        ↓
Linear Layer
        ↓
Class Prediction

πŸ§ͺ Practical Exercise 3 β€” Sequence Labeling

Modify the model so that it produces an output for every time step.

Expected:

Input:

x₁ xβ‚‚ x₃ xβ‚„

Output:

y₁ yβ‚‚ y₃ yβ‚„

πŸ§ͺ Practical Exercise 4 β€” Bidirectional RNN

Create:

nn.RNN(
    ...,
    bidirectional=True
)

Compare:

Unidirectional
vs
Bidirectional

on a sequence classification task.


πŸ§ͺ Practical Exercise 5 β€” Stacked RNN

Compare:

1 Layer
2 Layers
3 Layers

Measure:

Training Loss
Validation Loss
Accuracy
Training Time

πŸ§ͺ Practical Exercise 6 β€” Gradient Clipping

Train the same RNN:

Without Gradient Clipping

and:

With Gradient Clipping

Compare training stability.


πŸ§ͺ Practical Exercise 7 β€” Long-Term Dependency

Create a synthetic sequence task where the model must remember information from an early time step.

Experiment with:

Sequence Length = 10
Sequence Length = 50
Sequence Length = 100

Observe how vanilla RNN performance changes.


πŸ§ͺ Practical Exercise 8 β€” Time-Series Forecasting

Create a synthetic time series:

sin(t)

and train an RNN to predict:

next value

Compare:

Actual
vs
Predicted

πŸ§ͺ Practical Exercise 9 β€” Variable-Length Sequences

Create sequences of different lengths.

Implement:

Padding
+
Packed Sequences

and verify that the model processes only valid sequence positions.


πŸ§ͺ Practical Exercise 10 β€” RNN vs LSTM

Train:

Vanilla RNN

and:

LSTM

on a long-term dependency task.

Compare:

Training Stability
Validation Accuracy
Long-Term Dependency Performance

πŸ§ͺ Practical Exercise 11 β€” RNN vs Transformer

Use the same sequence classification dataset.

Compare:

RNN
vs
Transformer Encoder

Measure:

Accuracy
Training Time
Inference Latency
Memory

🧠 Interview Questions

Beginner

1. What is an RNN?

An RNN is a neural network architecture designed for sequential data that maintains a hidden state across time steps.

2. What is the hidden state?

The hidden state is a learned representation carrying information from previous time steps.

3. Why are RNNs useful for sequential data?

Because the current representation depends on both the current input and previous hidden state.

4. What is an RNN cell?

The computational unit that combines the current input and previous hidden state to produce a new hidden state.

5. What does unrolling an RNN mean?

It means representing the recurrent computation across individual time steps so that the sequence processing and backpropagation can be understood through time.


Intermediate

6. What is the basic RNN equation?

[ h_t=\tanh(W_{xh}x_t+W_{hh}h_{t-1}+b_h) ]

7. What is parameter sharing in an RNN?

The same recurrent weights are reused across all time steps.

8. What is a many-to-one RNN?

A sequence is processed to produce a single output, such as sentiment classification.

9. What is a many-to-many RNN?

A sequence produces outputs across multiple time steps, such as sequence labeling.

10. What is a Bidirectional RNN?

An RNN that processes the sequence in both forward and backward directions.

11. What is the main problem with vanilla RNNs?

They can suffer from vanishing and exploding gradients and have difficulty learning long-term dependencies.

12. What is gradient clipping?

A technique that limits gradient magnitude to reduce the risk of exploding gradients.


Advanced

13. What is Backpropagation Through Time?

BPTT applies backpropagation to the unrolled recurrent network across its time steps.

14. Why do RNNs suffer from vanishing gradients?

Repeated multiplication of recurrent derivatives can cause gradient magnitudes to shrink toward zero.

15. Why do RNNs suffer from exploding gradients?

Repeated multiplication can instead cause gradient magnitudes to grow rapidly.

16. Why do LSTMs help with long-term dependencies?

They introduce gated memory mechanisms that provide a more controlled path for retaining and updating information.

17. Why are RNNs difficult to parallelize across time?

The hidden state at time t depends on the hidden state from time t-1.

18. What is teacher forcing?

A training strategy where the actual previous target is provided as the next input rather than the model's previous prediction.

19. What is exposure bias?

The difference between training with ground-truth previous tokens and inference where the model must consume its own predictions.

20. When would a Bidirectional RNN be inappropriate?

When future information is unavailable at prediction time, such as strictly causal real-time streaming prediction.

21. What is the difference between an RNN and a Transformer?

An RNN processes sequences recurrently through hidden states, while a Transformer uses attention mechanisms to model relationships between tokens and can parallelize much of training.

22. Why are RNNs still relevant?

They remain useful for understanding sequential modeling and can still be appropriate for certain streaming, time-series, and resource-constrained workloads.


🏒 Enterprise Perspective

RNNs represent an important stage in the evolution of Deep Learning for sequential data.

The architectural progression is:

Feed-Forward Networks
        ↓
Vanilla RNN
        ↓
LSTM / GRU
        ↓
Attention
        ↓
Transformer
        ↓
Foundation Models

Understanding RNNs makes it easier to understand why:

Memory
+
Long-Term Dependencies
+
Parallelism
+
Attention

became increasingly important in modern AI systems.


🏒 Enterprise Sequence Modeling

A production sequence system may process:

Events
 ↓
Feature Pipeline
 ↓
Sequence Construction
 ↓
Embedding
 ↓
Sequence Model
 ↓
Prediction
 ↓
Business Decision

Examples include:

Fraud Detection
Demand Forecasting
Predictive Maintenance
User Behavior Prediction
Anomaly Detection
Speech Processing
Text Classification

🏒 Production Architecture Considerations

Before selecting an RNN, evaluate:

Sequence Length
State Requirements
Latency
Throughput
Training Parallelism
Model Size
Memory
Data Volume
Pretraining Availability
Monitoring Requirements

🏒 Production Insight

Production Insight

RNNs should not be selected simply because the input is sequential.

Modern sequence modeling provides several choices:

RNN
  ↓
LSTM / GRU
  ↓
Transformer

The architecture should be selected based on:

Sequence Length
+
Long-Term Dependency Requirements
+
Streaming Constraints
+
Training Scale
+
Latency
+
Infrastructure

RNNs can still be excellent for compact stateful workloads, but Transformers are often a stronger choice when long context, large-scale training, and parallelism dominate the requirements.


πŸ“Œ Key Takeaways

  • RNNs are designed to model sequential data.
  • RNNs maintain a hidden state across time steps.
  • The hidden state combines information from the current input and previous state.
  • The same recurrent parameters are reused at every time step.
  • RNNs can be unrolled across time for training.
  • Backpropagation Through Time is used to train recurrent networks.
  • RNNs support one-to-many, many-to-one, and many-to-many patterns.
  • Bidirectional RNNs process sequences in both directions.
  • Stacked RNNs can provide additional model depth.
  • Variable-length sequences require techniques such as padding, masking, and packed sequences.
  • Vanilla RNNs suffer from vanishing gradients.
  • Vanilla RNNs can also suffer from exploding gradients.
  • Gradient clipping can help control exploding gradients.
  • Vanilla RNNs struggle with long-term dependencies.
  • LSTM and GRU architectures were introduced to address important limitations of vanilla RNNs.
  • Teacher forcing can improve sequence-generation training but introduces exposure bias.
  • RNNs process sequences sequentially, limiting parallelism across time.
  • RNNs remain useful for certain streaming and stateful workloads.
  • Transformers provide stronger parallelism and global context modeling for many modern sequence tasks.
  • RNNs form an important conceptual foundation for understanding LSTM, GRU, attention, and Transformer architectures.

πŸ“š Further Reading

Continue with:

The next chapter introduces LSTM and GRU, gated recurrent architectures designed to address the long-term dependency and gradient-flow limitations of vanilla RNNs.


➑️ Next Chapter

25. LSTM and GRU


Enterprise AI Engineering Handbook
Building Production-Grade Enterprise AI Systems β€” One Chapter at a Time.