Skip to main content
TensorFlow intermediate Lesson 4 of 7

TensorFlow RNNs and LSTMs

Build sequence models in Keras — text generation, sentiment analysis, and time-series forecasting with LSTM and GRU layers.

Real-World Scenario

A news platform wants to classify articles into topics (Politics, Technology, Sports, etc.) from headline text. An LSTM processes each headline word by word, building a representation that captures context — “Apple releases new iPhone” is Technology, not Food. With 50,000 labeled headlines, bidirectional LSTM achieves 91% accuracy.

Sequence Preprocessing

import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences

# Sample text classification data
texts = [
    "president signs new climate bill into law",
    "tech giant releases latest smartphone model",
    "local team wins championship in overtime",
    "stock markets reach record high on earnings",
    "scientists discover new exoplanet in habitable zone",
    "quarterback throws for three hundred yards in victory",
    "central bank raises interest rates by half point",
    "researchers develop faster machine learning algorithm",
]
labels = [0, 1, 2, 3, 4, 2, 3, 1]  # 0=Politics, 1=Tech, 2=Sports, 3=Finance, 4=Science

# Tokenize
tokenizer = Tokenizer(num_words=5000, oov_token="<OOV>")
tokenizer.fit_on_texts(texts)
sequences = tokenizer.texts_to_sequences(texts)

# Pad to uniform length
MAX_LEN = 20
X = pad_sequences(sequences, maxlen=MAX_LEN, padding="post", truncating="post")
y = np.array(labels)

VOCAB_SIZE = len(tokenizer.word_index) + 1
print(f"Vocabulary size: {VOCAB_SIZE}")
print(f"X shape: {X.shape}")   # (8, 20)

LSTM Text Classifier

import tensorflow as tf
from tensorflow import keras
import numpy as np

VOCAB_SIZE  = 5000
MAX_LEN     = 20
EMBED_DIM   = 64
LSTM_UNITS  = 128
N_CLASSES   = 5

# Bidirectional LSTM — reads the sequence in both directions
model = keras.Sequential([
    keras.layers.Embedding(VOCAB_SIZE, EMBED_DIM, mask_zero=True,
                           input_length=MAX_LEN),
    keras.layers.SpatialDropout1D(0.2),      # drops entire feature maps

    # Stack 2 LSTM layers — first must return sequences
    keras.layers.Bidirectional(
        keras.layers.LSTM(LSTM_UNITS, return_sequences=True, dropout=0.2)
    ),
    keras.layers.Bidirectional(
        keras.layers.LSTM(LSTM_UNITS // 2, dropout=0.2)  # last layer: no return_sequences
    ),

    keras.layers.Dense(64, activation="relu"),
    keras.layers.Dropout(0.3),
    keras.layers.Dense(N_CLASSES, activation="softmax"),
])

model.compile(
    optimizer=keras.optimizers.Adam(1e-3),
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"],
)
model.summary()

# Training with dummy data
X_train = np.random.randint(0, VOCAB_SIZE, (800, MAX_LEN))
y_train = np.random.randint(0, N_CLASSES, 800)
X_val   = np.random.randint(0, VOCAB_SIZE, (200, MAX_LEN))
y_val   = np.random.randint(0, N_CLASSES, 200)

history = model.fit(
    X_train, y_train,
    validation_data=(X_val, y_val),
    epochs=20,
    batch_size=32,
    callbacks=[
        keras.callbacks.EarlyStopping(patience=4, restore_best_weights=True),
        keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=2, verbose=1),
    ],
    verbose=1,
)

Time-Series Forecasting with LSTM

import numpy as np
import tensorflow as tf
from tensorflow import keras

# Generate synthetic multivariate time series
# Features: temperature, humidity, pressure → predict temperature
rng  = np.random.default_rng(42)
N    = 2000

temperature = 20 + 10 * np.sin(np.arange(N) * 2 * np.pi / 365) + rng.normal(0, 1, N)
humidity    = 60 + 20 * np.cos(np.arange(N) * 2 * np.pi / 365) + rng.normal(0, 2, N)
pressure    = 1013 + rng.normal(0, 5, N)

data = np.stack([temperature, humidity, pressure], axis=1)  # (N, 3)

# Normalize
mean, std = data.mean(0), data.std(0)
data = (data - mean) / std

LOOKBACK   = 30   # days of history
HORIZON    = 1    # days to predict

def make_sequences(data: np.ndarray, lookback: int, horizon: int):
    X, y = [], []
    for i in range(len(data) - lookback - horizon + 1):
        X.append(data[i:i + lookback])              # (lookback, features)
        y.append(data[i + lookback, 0])             # predict temperature only
    return np.array(X), np.array(y)

X, y = make_sequences(data, LOOKBACK, HORIZON)
split = int(len(X) * 0.8)
X_train, X_val = X[:split], X[split:]
y_train, y_val = y[:split], y[split:]

# Functional API — cleaner for multi-input/output architectures
inputs = keras.Input(shape=(LOOKBACK, 3))
x = keras.layers.LSTM(64, return_sequences=True)(inputs)
x = keras.layers.Dropout(0.2)(x)
x = keras.layers.LSTM(32)(x)
x = keras.layers.Dense(16, activation="relu")(x)
outputs = keras.layers.Dense(1)(x)

model = keras.Model(inputs, outputs)
model.compile(
    optimizer=keras.optimizers.Adam(1e-3),
    loss="mse",
    metrics=["mae"],
)

history = model.fit(
    X_train, y_train,
    validation_data=(X_val, y_val),
    epochs=50,
    batch_size=64,
    callbacks=[
        keras.callbacks.EarlyStopping(patience=8, restore_best_weights=True),
        keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=4),
    ],
    verbose=1,
)

# Evaluate
val_loss, val_mae = model.evaluate(X_val, y_val, verbose=0)
# Denormalize MAE back to original scale
real_mae = val_mae * std[0]
print(f"Validation MAE: {real_mae:.2f}°C")

Character-Level Text Generation with GRU

import tensorflow as tf
from tensorflow import keras
import numpy as np

TEXT = """
To be or not to be that is the question whether tis nobler in the mind to suffer
the slings and arrows of outrageous fortune or to take arms against a sea of troubles
and by opposing end them to die to sleep no more and by a sleep to say we end
""" * 10  # repeat for enough training data

chars       = sorted(set(TEXT))
char2idx    = {c: i for i, c in enumerate(chars)}
idx2char    = {i: c for c, i in char2idx.items()}
VOCAB_SIZE  = len(chars)

# Create input/target pairs
SEQ_LEN = 40
X_chars, y_chars = [], []
for i in range(0, len(TEXT) - SEQ_LEN - 1, 3):
    X_chars.append([char2idx[c] for c in TEXT[i:i + SEQ_LEN]])
    y_chars.append(char2idx[TEXT[i + SEQ_LEN]])

X_data = np.array(X_chars)
y_data = np.array(y_chars)

# GRU-based language model
model = keras.Sequential([
    keras.layers.Embedding(VOCAB_SIZE, 32, input_length=SEQ_LEN),
    keras.layers.GRU(128, return_sequences=True),
    keras.layers.GRU(64),
    keras.layers.Dense(VOCAB_SIZE, activation="softmax"),
])

model.compile(
    optimizer=keras.optimizers.Adam(1e-3),
    loss="sparse_categorical_crossentropy",
)

model.fit(X_data, y_data, epochs=30, batch_size=128, verbose=1)


def generate_text(model, seed: str, length: int = 200, temperature: float = 0.8) -> str:
    """Generate text by sampling from the model's output distribution."""
    generated = seed
    sequence  = [char2idx.get(c, 0) for c in seed[-SEQ_LEN:]]

    for _ in range(length):
        x = np.array([sequence[-SEQ_LEN:]])
        probs = model.predict(x, verbose=0)[0]

        # Temperature scaling: higher = more random, lower = more focused
        probs = np.log(probs + 1e-10) / temperature
        probs = np.exp(probs) / np.exp(probs).sum()

        next_idx = np.random.choice(len(probs), p=probs)
        generated += idx2char[next_idx]
        sequence.append(next_idx)

    return generated


print(generate_text(model, seed="to be or", length=200, temperature=0.7))

Frequently Asked Questions

How do I handle variable-length sequences in Keras?
Use the Masking layer or set mask_zero=True on your Embedding layer. Keras will then propagate the mask through all subsequent layers that support it (LSTM, GRU, Bidirectional). Pad sequences to the same length with pad_sequences() first, then Keras handles the masking automatically during the forward pass.
What is the return_sequences parameter in LSTM?
return_sequences=False (default) returns only the last hidden state — use this for classification where you need one output per sequence. return_sequences=True returns the hidden state at every time step — use this when stacking LSTM layers (all but the last need this) or for sequence-to-sequence tasks like translation.