Recurrent Neural Networks and Sequential Learning
learning how recurrent memory processes ordered data, time series, and language
Read diagram labels
- Hidden states: h₁ → h₂ → h₃
- Inputs: x₁, x₂, x₃
- Prediction: ŷ
- The same recurrent weights are reused at every step
Core Concepts Covered
- Sequential tensors, recurrent weights, and hidden-state memory
- Backpropagation Through Time and long-sequence gradient challenges
- Time-series prediction, text classification, LSTMs, and GRUs
Local Setup Recommendation
To execute and experiment with the code cells below on your local machine, ensure you have set up your isolated virtual environments and scientific libraries by following the detailed protocols in Unit 03: Environment Setup or run them in Google Colab.
1. Sequences, Recurrence, and Hidden State
Recurrent Neural Networks (RNNs) are designed for ordered data such as text, audio, sensor readings, and time series. A feedforward network treats each input as independent, while an RNN reads a sequence one step at a time and carries a hidden state forward as a compact memory of what it has already seen.
At time step , a basic RNN combines the current input with the previous hidden state : The same recurrent weights are reused at every time step. This parameter sharing lets one model process sequences of different lengths and learn patterns whose meaning depends on order.
With batch_first=True, PyTorch expects an input tensor shaped as:
For example, represents 32 sequences, 20 time steps per sequence, and one feature at each step.
The code cells in this unit are static demonstrations and do not execute on this website. You can copy them into your own Google Colab notebook, or use the local environment configured in Unit 03, to run and modify them. Plotting examples require Matplotlib.
import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
# Reproducible notebook results
torch.manual_seed(42)
np.random.seed(42)
print("PyTorch environment ready!")Example output (precomputed; this website does not execute code):
PyTorch environment ready!2. Sine-Wave Data: Predicting the Next Point
To make recurrence concrete, begin with a predictable sine wave. The learning task is next-step prediction: use a window of past values as and predict the following value . For a one-step window, this is , then , and so on.
Setting seq_length = 1 is useful for inspecting tensor shapes and the mechanics of one recurrent update, but it gives the model only one observed value per example. It cannot demonstrate memory across a long history. After understanding the pipeline, try a longer window such as so the hidden state can summarize a meaningful portion of the wave.
The optional plot marks the input window and the next-value target. The same sliding-window transformation is used for demand forecasting, sensor prediction, and many other time-series problems.
# Create a sine wave and convert it into past-to-future pairs.
x_axis = np.linspace(0, 100, 1000)
wave_data = np.sin(x_axis)
def create_sequences(data, seq_length):
xs, ys = [], []
for i in range(len(data) - seq_length):
xs.append(data[i:i + seq_length])
ys.append(data[i + seq_length])
return np.array(xs), np.array(ys)
seq_length = 1
X_raw, Y_raw = create_sequences(wave_data, seq_length)
# Add the feature axis expected by nn.RNN.
X = torch.tensor(X_raw, dtype=torch.float32).unsqueeze(-1)
Y = torch.tensor(Y_raw, dtype=torch.float32).unsqueeze(-1)
print("X shape:", tuple(X.shape))
print("Y shape:", tuple(Y.shape))
print("First input:", np.round(X_raw[0], 3))
print("First target:", round(float(Y_raw[0]), 3))
# Optional Colab visualization of one sliding window.
start = 30
end = start + seq_length
plt.figure(figsize=(10, 4))
plt.plot(wave_data[:120], label="Sine wave")
plt.scatter(range(start, end), wave_data[start:end], label="Input window")
plt.scatter(end, wave_data[end], marker="*", s=160, label="Next-value target")
plt.xlabel("Time step")
plt.ylabel("Wave amplitude")
plt.legend()
plt.grid(True)
plt.show()Example output (precomputed; plot appears only when run in Colab):
X shape: (999, 1, 1)
Y shape: (999, 1)
First input: [0.]
First target: 0.13. Building the Wave RNN and Reading Its Memory
nn.RNN performs the repeated recurrence automatically. Its out tensor stores the hidden state from every time step, while hidden stores the final hidden state for every recurrent layer. A prediction head can transform the last layer's final state into the desired output.
For one example shaped and a hidden size of , out has shape : one batch, one time step, and 32 hidden features. hidden has shape : one recurrent layer, one batch item, and 32 final hidden features. With a sequence length of , out would retain 20 states while hidden would still retain only the final state per layer.
class WaveRNN(nn.Module):
def __init__(self, input_size=1, hidden_size=32, output_size=1):
super().__init__()
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
all_hidden_states, final_hidden = self.rnn(x)
prediction = self.fc(final_hidden[-1])
return prediction
model = WaveRNN()
one_sequence = X[0:1] # (one example, one time step, one feature)
with torch.no_grad():
all_hidden_states, final_hidden = model.rnn(one_sequence)
print(model)
print("Input shape:", tuple(one_sequence.shape))
print("All hidden states:", tuple(all_hidden_states.shape))
print("Final hidden state:", tuple(final_hidden.shape))Example output (precomputed):
WaveRNN(
(rnn): RNN(1, 32, batch_first=True)
(fc): Linear(in_features=32, out_features=1, bias=True)
)
Input shape: (1, 1, 1)
All hidden states: (1, 1, 32)
Final hidden state: (1, 1, 32)4. Training with Backpropagation Through Time
RNN training follows the usual loop: predict, measure loss, backpropagate, and update the weights. Because the recurrent cell is conceptually unfolded across the sequence, the backward pass is called Backpropagation Through Time (BPTT). Error signals travel backward through the repeated time steps so shared weights learn how earlier inputs influenced the final prediction.
PyTorch Autograd performs BPTT automatically when loss.backward() is called. The sine-wave task uses Mean Squared Error because its target is continuous. The plots below are also static code: when copied into Colab, one shows whether loss decreases and the other compares the model's predictions with the true wave.
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
loss_history = []
for epoch in range(150):
model.train()
predictions = model(X)
loss = criterion(predictions, Y)
optimizer.zero_grad()
loss.backward() # Autograd performs BPTT.
optimizer.step()
loss_history.append(loss.item())
if epoch % 25 == 0:
print(f"Epoch {epoch:3d} | Loss: {loss.item():.5f}")
model.eval()
with torch.no_grad():
wave_predictions = model(X).numpy()
# Plot training loss.
plt.figure(figsize=(8, 4))
plt.plot(loss_history)
plt.xlabel("Epoch")
plt.ylabel("MSE loss")
plt.grid(True)
plt.show()
# Plot the first 200 targets and predictions.
plt.figure(figsize=(10, 4))
plt.plot(Y.numpy()[:200], label="Actual wave", linewidth=3, alpha=0.5)
plt.plot(wave_predictions[:200], "r--", label="RNN prediction")
plt.xlabel("Time step")
plt.ylabel("Wave amplitude")
plt.legend()
plt.grid(True)
plt.show()Example output (illustrative; exact losses depend on the environment):
Epoch 0 | Loss: decreases from its initial value
Epoch 25 | Loss: lower than the initial value
...
The plots are generated only after this code is copied and run in Colab.5. Long Sequences, Vanishing Gradients, LSTMs, and GRUs
A basic RNN can struggle with long dependencies. During BPTT, gradients repeatedly multiply through recurrent transformations. If their magnitudes are mostly below , they can shrink toward zero; if mostly above , they can grow explosively. Vanishing gradients make early events difficult to learn, while exploding gradients can destabilize training. Gradient clipping helps control explosions but does not restore information already lost through vanishing gradients.
Long Short-Term Memory (LSTM) networks introduce a cell state plus input, forget, and output gates that regulate what information is written, preserved, and exposed. Gated Recurrent Units (GRUs) use a simpler update/reset gating design. Both make longer dependencies easier to learn than a plain RNN, although recurrent models still process positions sequentially and are less parallel than Transformers.
6. From Words to Ordered Numerical Sequences
Text demonstrates why order matters: “not good” and “not bad” contain similar words but convey different meanings. An RNN reads tokens in order, allowing earlier words to change the hidden state used to interpret later ones.
The static example below uses NLTK's Movie Reviews corpus. A vocabulary maps words to integer IDs, unknown words use <UNK>, and shorter reviews are padded with <PAD> until every batch has a consistent length. When copied to Colab, it requires NLTK and downloads the movie_reviews dataset.
# Colab note: this downloads the NLTK Movie Reviews dataset when run.
import nltk
from nltk.corpus import movie_reviews
from collections import Counter
import random
nltk.download("movie_reviews", quiet=True)
documents = []
for label in movie_reviews.categories():
for file_id in movie_reviews.fileids(label):
words = [w.lower() for w in movie_reviews.words(file_id) if w.isalpha()]
documents.append((words, 1 if label == "pos" else 0))
random.seed(42)
random.shuffle(documents)
word_counts = Counter(word for words, _ in documents for word in words)
vocab = {"<PAD>": 0, "<UNK>": 1}
for word, _ in word_counts.most_common(9_998):
vocab[word] = len(vocab)
max_len = 100
def encode_review(words):
ids = [vocab.get(word, vocab["<UNK>"]) for word in words[:max_len]]
return ids + [vocab["<PAD>"]] * (max_len - len(ids))
X_text = torch.tensor([encode_review(words) for words, _ in documents])
Y_text = torch.tensor([label for _, label in documents], dtype=torch.float32).unsqueeze(1)
split = int(0.8 * len(X_text))
X_train, X_test = X_text[:split], X_text[split:]
Y_train, Y_test = Y_text[:split], Y_text[split:]
print("Reviews:", len(documents))
print("Encoded input shape:", tuple(X_text.shape))Example output (precomputed; dataset downloads only when run in Colab):
Reviews: 2000
Encoded input shape: (2000, 100)7. Static NLP Example: Sentiment Classification
The sentiment model has three parts: an embedding layer converts word IDs into learned vectors, an RNN processes those vectors in order, and a linear layer converts the final hidden state into one classification logit. BCEWithLogitsLoss combines a numerically stable sigmoid operation with binary cross-entropy during training.
After training, applying sigmoid converts each logit into a probability. This is a deliberately small educational model, so learners should expect errors on sarcasm, rare words, long dependencies, and sentences that differ from the training corpus.
class SentimentRNN(nn.Module):
def __init__(self, vocab_size, embedding_dim=64, hidden_size=64):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=0)
self.rnn = nn.RNN(embedding_dim, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, 1)
def forward(self, token_ids):
embedded = self.embedding(token_ids)
_, final_hidden = self.rnn(embedded)
return self.fc(final_hidden[-1])
sentiment_model = SentimentRNN(len(vocab))
criterion = nn.BCEWithLogitsLoss()
optimizer = torch.optim.Adam(sentiment_model.parameters(), lr=0.001)
for epoch in range(5):
sentiment_model.train()
permutation = torch.randperm(X_train.size(0))
for start in range(0, X_train.size(0), 64):
indices = permutation[start:start + 64]
logits = sentiment_model(X_train[indices])
loss = criterion(logits, Y_train[indices])
optimizer.zero_grad()
loss.backward()
optimizer.step()
def predict_sentiment(sentence):
words = [word.lower() for word in sentence.split() if word.isalpha()]
token_ids = torch.tensor([encode_review(words)])
sentiment_model.eval()
with torch.no_grad():
probability = torch.sigmoid(sentiment_model(token_ids)).item()
label = "positive" if probability >= 0.5 else "negative"
return label, probability
print(predict_sentiment("the movie was great and good"))
print(predict_sentiment("the movie was long and boring"))Example output only; predictions appear after training in Colab:
('positive', model_probability)
('negative', model_probability)Interactive Practice Quiz
Test your understanding with instant feedback
With batch_first=True, what does an RNN input shape of mean?
What does an RNN's final hidden state represent?
Why is RNN backpropagation called Backpropagation Through Time (BPTT)?
What is the main limitation of using seq_length = 1 for sine-wave prediction?
Why do LSTMs and GRUs often learn long-range dependencies better than plain RNNs?
Further Readings
Explore these highly recommended external references to deepen your understanding
