LSTMによる時系列予測:理論とPython実装

LSTM(Long Short-Term Memory)の忘却・入力・出力ゲートの数学的基礎から、PyTorchによる時系列予測の実装、ARIMAとの比較まで体系的に解説します。

はじめに

時系列データの予測において、ARIMAモデルは古典的な手法として広く使われてきました。しかし、非線形な依存関係や長期的なパターンを捉えるには限界があります。

**LSTM(Long Short-Term Memory)**は、再帰型ニューラルネットワーク(RNN)の一種であり、勾配消失問題を解決し、長期依存関係を学習できるアーキテクチャです。本記事では、LSTMの数学的基礎からPyTorchによる実践的な実装までを解説します。

RNNの限界:勾配消失問題

通常のRNNは、時刻 \(t\) の隠れ状態 \(h_t\) を次式で更新します。

\[h_t = \tanh(W_h h_{t-1} + W_x x_t + b) \tag{1}\]

バックプロパゲーション・スルー・タイム(BPTT)では、誤差勾配が時刻をさかのぼって伝播します。時系列が長いと、勾配が \(T\) ステップ分だけ繰り返し行列積をとるため、固有値の絶対値が 1 未満だと指数的に小さくなります(勾配消失)。

LSTMはセル状態 \(c_t\) という「記憶ライン」を導入することでこの問題を解決します。

LSTMのアーキテクチャ

LSTMは1つのセルに3つのゲートを持ちます。入力は現在の入力 \(x_t\) と前時刻の隠れ状態 \(h_{t-1}\) です。

忘却ゲート (Forget Gate)

過去のセル状態のどの情報を忘れるかを決定します。

\[f_t = \sigma(W_f [h_{t-1}, x_t] + b_f) \tag{2}\]

\(\sigma\) はシグモイド関数で、出力は \([0, 1]\) の範囲です。0 に近いほど「忘れる」、1 に近いほど「保持する」を意味します。

入力ゲート (Input Gate)

新しい情報をセル状態にどれだけ書き込むかを制御します。

\[i_t = \sigma(W_i [h_{t-1}, x_t] + b_i) \tag{3}\]\[\tilde{c}_t = \tanh(W_c [h_{t-1}, x_t] + b_c) \tag{4}\]

セル状態の更新

忘却ゲートと入力ゲートを組み合わせてセル状態を更新します。

\[c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t \tag{5}\]

ここで \(\odot\) はアダマール積(要素ごとの積)です。この加算形の更新式により、勾配が長期間にわたって流れやすくなります。

出力ゲート (Output Gate)

セル状態から何を出力するかを決定します。

\[o_t = \sigma(W_o [h_{t-1}, x_t] + b_o) \tag{6}\]\[h_t = o_t \odot \tanh(c_t) \tag{7}\]

パラメータ数

入力次元を \(d_x\)、隠れ層次元を \(d_h\) とすると、1つのLSTMセルのパラメータ数は次のようになります。

\[4 \times (d_h \times (d_x + d_h) + d_h) \tag{8}\]

4つの行列(\(W_f, W_i, W_c, W_o\))それぞれに \(d_h \times (d_x + d_h)\) の重みと \(d_h\) のバイアスがあります。

時系列予測へのLSTMの適用

時系列予測では、過去 \(T\) 時点の値 \(\{x_{t-T+1}, \ldots, x_t\}\) を入力として次時点 \(x_{t+1}\) を予測するSeq2One構成が基本です。

構成入力出力用途
Seq2One系列 → 1点1時点予測単ステップ予測
Seq2Seq系列 → 系列複数時点予測多ステップ予測
Encoder-Decoder可変長入力可変長出力機械翻訳・異常検知

Python実装(PyTorch)

データ準備

正弦波にノイズを加えた合成データで予測を行います。

import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
import matplotlib.pyplot as plt

# 合成時系列データ生成
np.random.seed(42)
t = np.linspace(0, 8 * np.pi, 1000)
data = np.sin(t) + 0.2 * np.random.randn(len(t))

# 正規化
data_mean, data_std = data.mean(), data.std()
data_norm = (data - data_mean) / data_std

def create_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)

SEQ_LEN = 30
X, y = create_sequences(data_norm, SEQ_LEN)

# 訓練・テスト分割 (80/20)
split = int(len(X) * 0.8)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]

# PyTorchテンソルに変換 (shape: [batch, seq_len, features])
X_train_t = torch.FloatTensor(X_train).unsqueeze(-1)
X_test_t  = torch.FloatTensor(X_test).unsqueeze(-1)
y_train_t = torch.FloatTensor(y_train).unsqueeze(-1)
y_test_t  = torch.FloatTensor(y_test).unsqueeze(-1)

train_loader = DataLoader(
    TensorDataset(X_train_t, y_train_t),
    batch_size=32, shuffle=True
)

LSTMモデル定義

class LSTMForecaster(nn.Module):
    def __init__(self, input_size=1, hidden_size=64, num_layers=2, dropout=0.2):
        super().__init__()
        self.lstm = nn.LSTM(
            input_size=input_size,
            hidden_size=hidden_size,
            num_layers=num_layers,
            batch_first=True,
            dropout=dropout if num_layers > 1 else 0.0
        )
        self.fc = nn.Linear(hidden_size, 1)

    def forward(self, x):
        # x: [batch, seq_len, input_size]
        out, _ = self.lstm(x)
        # 最後の時刻の隠れ状態を使用
        return self.fc(out[:, -1, :])

model = LSTMForecaster(hidden_size=64, num_layers=2)
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

学習ループ

EPOCHS = 50
train_losses = []

for epoch in range(EPOCHS):
    model.train()
    epoch_loss = 0.0
    for X_batch, y_batch in train_loader:
        optimizer.zero_grad()
        pred = model(X_batch)
        loss = criterion(pred, y_batch)
        loss.backward()
        # 勾配クリッピング(RNN系では有効)
        nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        optimizer.step()
        epoch_loss += loss.item()
    train_losses.append(epoch_loss / len(train_loader))

    if (epoch + 1) % 10 == 0:
        print(f"Epoch {epoch+1}/{EPOCHS}  Loss: {train_losses[-1]:.6f}")

予測と評価

model.eval()
with torch.no_grad():
    pred_norm = model(X_test_t).numpy().squeeze()

# 逆正規化
pred = pred_norm * data_std + data_mean
true = y_test * data_std + data_mean

# RMSE
rmse = np.sqrt(np.mean((pred - true) ** 2))
print(f"RMSE: {rmse:.4f}")

# 可視化
plt.figure(figsize=(12, 4))
plt.plot(true, label="真値", alpha=0.7)
plt.plot(pred, label="LSTM予測", alpha=0.7)
plt.xlabel("時刻")
plt.ylabel("値")
plt.title("LSTMによる時系列予測")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

ARIMAとLSTMの比較

観点ARIMALSTM
前提条件定常性(差分で対応)なし
非線形モデリング不可可能
長期依存関係弱い(低次ARIMAの場合)セル状態で保持
パラメータ数少(p, d, q 程度)多(数千〜数万)
解釈可能性高い(係数の意味が明確)低い(ブラックボックス)
学習データ量少量でも可多量が必要
学習時間速い遅い(GPUで高速化可)
推論時間速い中程度
過学習リスク低い高い(Dropoutで対策)

選択の指針:

  • データ量が少ない・線形な季節性がある → ARIMA/SARIMA
  • 非線形・複雑なパターン・多変量 → LSTM
  • リアルタイム推論が必要 → 軽量なLSTMまたはARIMA

多変量時系列への拡張

複数の特徴量を使う場合は input_size を変更するだけです。

# 例: 温度・湿度・気圧の3変数から翌時刻の温度を予測
model = LSTMForecaster(input_size=3, hidden_size=128, num_layers=2)
# X_train_t: [batch, seq_len, 3]

関連記事

参考

  • Hochreiter, S., & Schmidhuber, J. (1997). Long Short-Term Memory. Neural Computation, 9(8), 1735–1780.
  • PyTorch Documentation: torch.nn.LSTM

関連ツール