Imagine reading a detective novel. On page 200, a clue makes sense only because of something mentioned on page 12. Your brain seamlessly holds context across hundreds of pages, connecting distant pieces of information to build understanding. Standard neural networks cannot do this — they read page 200 with no memory of pages 1 through 199. Recurrent Neural Networks (RNNs) were built to fix exactly this limitation, and LSTM is the variant that actually works well for long sequences.
Understanding RNNs and LSTMs matters in 2026 even though Transformers have displaced them in most NLP tasks, because LSTMs remain the workhorse for time series forecasting in industry — predicting energy consumption, stock movements, equipment failure, sales demand, and IoT sensor anomalies. If you work with sequential data, you will encounter these architectures.
Why Standard Neural Networks Cannot Handle Sequences
A feedforward neural network processes each input independently. Feed it the words “The bank raised interest rates” and it classifies each word in isolation, with no awareness that “bank” appears in a financial context established by “interest rates” later in the sentence. For isolated classification tasks — is this image a cat? — this is fine. For sequential tasks, it is fatal.
Time series data has the same problem. Tomorrow’s electricity demand depends not just on today’s weather, but on patterns over the past week, month, and year. A model that only sees today’s features and ignores this history will be a poor forecaster. Sequential models solve this by maintaining state — a form of memory that carries information from one time step to the next.
How an RNN Works
An RNN processes sequences one time step at a time. At each step, it receives two inputs: the current data point, and a hidden state from the previous step. The hidden state is a compressed representation of everything the network has seen so far — its working memory. The network updates this hidden state at each step and uses it to produce a prediction.
Think of reading the temperature readings from a weather sensor every hour. An RNN processes each reading sequentially. After reading 9am data, it updates its hidden state. After reading 10am data, it updates the hidden state again — now encoding patterns from both 9am and 10am. By the time it reads 6pm data, the hidden state encodes a summary of the entire day’s pattern, which the network uses to forecast 7pm.
The problem with vanilla RNNs is that hidden states lose information rapidly. By the time the network reaches time step 100, the hidden state contains almost nothing from time step 1. Gradients vanish exponentially as they travel backward through time during training, making it effectively impossible for the network to learn relationships spanning more than roughly 20-30 time steps. This is the vanishing gradient problem.
LSTM: Solving the Long-Term Memory Problem
Long Short-Term Memory networks, introduced by Hochreiter and Schmidhuber in 1997, solve the vanishing gradient problem with an architectural change: a separate cell state that carries long-term information alongside the standard hidden state. The cell state flows through time with minimal modification, allowing gradients to flow backward without vanishing.
Three gates control what information flows into, out of, and through the cell state. The forget gate decides which information in the cell state is no longer relevant and should be erased. The input gate decides what new information from the current step should be written into the cell state. The output gate decides which parts of the cell state should be used to produce the current output.
This gating mechanism gives the LSTM explicit control over its memory. It can remember information from hundreds of time steps ago when it is relevant, and forget it when the context changes. For our weather forecasting example, an LSTM might learn to remember seasonal patterns spanning weeks while quickly forgetting irrelevant short-term noise.
Building an LSTM for Time Series Forecasting in Python
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_absolute_error, mean_squared_error
import matplotlib.pyplot as plt
# ── Generate realistic sales data ────────────────────────────────
np.random.seed(42)
n = 730 # 2 years of daily data
t = np.arange(n)
trend = 0.3 * t
seasonal = 25 * np.sin(2 * np.pi * t / 365) # Annual seasonality
weekly = 10 * np.sin(2 * np.pi * t / 7) # Weekly seasonality
noise = np.random.normal(0, 8, n)
sales = 200 + trend + seasonal + weekly + noise
print(f'Sales range: {sales.min():.1f} to {sales.max():.1f}')
print(f'Mean: {sales.mean():.1f}')
plt.figure(figsize=(13, 4))
plt.plot(t[:200], sales[:200], linewidth=1.2, color='steelblue')
plt.xlabel('Day'); plt.ylabel('Sales Units')
plt.title('Synthetic Sales Data — First 200 Days')
plt.tight_layout()
plt.show()
# ── Prepare sequences ────────────────────────────────────────────
# Normalise to [0, 1] — LSTMs train better with small input values
scaler = MinMaxScaler(feature_range=(0, 1))
sales_scaled = scaler.fit_transform(sales.reshape(-1, 1)).flatten()
SEQ_LEN = 60 # Use 60 days of history to predict the next day
def make_sequences(data, seq_len):
X, y = [], []
for i in range(len(data) - seq_len):
X.append(data[i : i + seq_len])
y.append(data[i + seq_len])
return np.array(X), np.array(y)
X, y = make_sequences(sales_scaled, SEQ_LEN)
X = X.reshape(X.shape[0], X.shape[1], 1) # (samples, timesteps, features)
# Keep temporal order — NEVER shuffle time series data
split = int(len(X) * 0.8)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]
print(f'Training samples: {len(X_train)}')
print(f'Test samples: {len(X_test)}')
# ── Build LSTM model ─────────────────────────────────────────────
model = keras.Sequential([
keras.layers.LSTM(
128,
input_shape=(SEQ_LEN, 1),
return_sequences=True # Pass full sequence to next LSTM layer
),
keras.layers.Dropout(0.2),
keras.layers.LSTM(
64,
return_sequences=False # Only return last hidden state
),
keras.layers.Dropout(0.2),
keras.layers.Dense(32, activation='relu'),
keras.layers.Dense(1) # Single output: next day's sales
])
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=0.001),
loss='mse',
metrics=['mae']
)
model.summary()
# Early stopping prevents overfitting
early_stop = keras.callbacks.EarlyStopping(
monitor='val_loss', patience=20,
restore_best_weights=True
)
history = model.fit(
X_train, y_train,
epochs=150,
batch_size=32,
validation_data=(X_test, y_test),
callbacks=[early_stop],
verbose=0
)
print(f'
Training stopped at epoch {len(history.history["loss"])}')
# ── Evaluate ────────────────────────────────────────────────────
y_pred_scaled = model.predict(X_test).flatten()
# Inverse transform back to original scale
y_pred = scaler.inverse_transform(y_pred_scaled.reshape(-1, 1)).flatten()
y_true = scaler.inverse_transform(y_test.reshape(-1, 1)).flatten()
mae = mean_absolute_error(y_true, y_pred)
rmse = np.sqrt(mean_squared_error(y_true, y_pred))
mape = np.mean(np.abs((y_true - y_pred) / y_true)) * 100
print(f'MAE: {mae:.2f} units')
print(f'RMSE: {rmse:.2f} units')
print(f'MAPE: {mape:.2f}%')
# Plot predictions vs actual for last 90 days
plt.figure(figsize=(13, 5))
plt.plot(y_true[-90:], label='Actual Sales', color='steelblue', linewidth=1.5)
plt.plot(y_pred[-90:], label='LSTM Forecast', color='coral', linewidth=1.5, linestyle='--')
plt.xlabel('Day'); plt.ylabel('Sales Units')
plt.title(f'LSTM Sales Forecast — MAPE: {mape:.2f}%')
plt.legend(); plt.tight_layout(); plt.show()
Choosing the Right Model for Your Time Series Problem
LSTM is not always the right tool. Simpler models are faster to train, easier to explain, and often competitive — especially on smaller datasets. Before committing to an LSTM, benchmark these alternatives:
| Model | Best For | Limitations |
|---|---|---|
| ARIMA / SARIMA | Short stationary series, strong seasonality | Univariate only, manual parameter selection |
| Prophet (Facebook) | Daily data with weekly/yearly seasonality and holidays | Less flexible for complex multivariate patterns |
| XGBoost with lag features | Tabular time series with many external features | Requires manual feature engineering of lags and rolling stats |
| LSTM | Long-range dependencies, multivariate, multi-step forecasting | Needs substantial data, slow to train and tune |
| Temporal Fusion Transformer | Large multivariate datasets, interpretable attention | Complex implementation, needs large datasets |
Frequently Asked Questions
Should I use LSTM or Transformers for time series forecasting in 2026?
It depends on your dataset size and complexity. LSTMs are the practical choice for most industry time series problems — they are well-understood, have robust library support, train reasonably fast, and perform well on datasets with thousands to hundreds of thousands of time steps. Transformer-based architectures (Temporal Fusion Transformer, PatchTST, iTransformer) consistently outperform LSTMs on large, complex multivariate forecasting benchmarks, but they require substantially more data to show that advantage, are more complex to implement and tune, and can underperform simpler methods on small datasets. The practical recommendation: start with LSTM (or even simpler baselines like XGBoost with lag features), and switch to Transformers only if you have a large dataset and LSTM performance is insufficient.
What does return_sequences=True actually do, and when do I need it?
Without return_sequences, an LSTM layer processes the entire input sequence and returns only the hidden state from the final time step — a single vector summarising the whole sequence. With return_sequences=True, it returns the hidden state from every time step — a sequence of vectors, one per input time step. You need return_sequences=True when stacking LSTM layers (the second LSTM needs to receive a full sequence, not just a summary), or when your output is itself a sequence (many-to-many problems like translation). Use return_sequences=False on the last LSTM layer before a Dense output layer, and True on all preceding LSTM layers in a stacked architecture.
How long should my sequence length (look-back window) be?
Start with a window that covers one to two full seasonal cycles. For daily sales data with weekly patterns, try 14 or 30 days. For hourly energy data with daily patterns, try 24 or 48 hours. For monthly financial data, try 12 or 24 months. Treat sequence length as a hyperparameter and use cross-validation to compare options. Too short a window and the model cannot learn meaningful patterns. Too long and training slows dramatically with diminishing returns as older time steps carry less relevant information. Optuna can efficiently search over sequence length as part of broader hyperparameter tuning.
Why do I need to normalise data before training an LSTM?
LSTMs (and neural networks in general) are sensitive to the scale of input values. If your sales data ranges from 100 to 100,000, the large values will produce very large activations early in training, causing gradients to become unstable and training to diverge or converge very slowly. Normalising to [0, 1] with MinMaxScaler or standardising to zero mean and unit variance with StandardScaler keeps all input values in a range where the network’s activations and gradients remain well-behaved throughout training. Always fit the scaler on training data only, then apply it to both training and test data — fitting on combined data leaks future information into the scaler and inflates your performance estimates.



