はじめに
時系列データの予測において、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の比較
| 観点 | ARIMA | LSTM |
|---|---|---|
| 前提条件 | 定常性(差分で対応) | なし |
| 非線形モデリング | 不可 | 可能 |
| 長期依存関係 | 弱い(低次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]
関連記事
- 時系列予測の基礎:ARIMAモデルの理論とPython実装 - 線形時系列モデルの古典的手法。LSTMと比較すると解釈性の高さが特徴です。
- 遺伝的アルゴリズムによるニューラルネットワーク学習のPython実装 - 勾配を使わずにニューラルネットワークを最適化するアプローチ。
- 確率的勾配降下法とAdamの理論とPython実装 - LSTMの学習に使うAdamオプティマイザの詳細。
- 時系列データの異常検知:統計的手法からカルマンフィルタまで - LSTMを使った異常検知にも応用できる手法群。
- Self-Attentionの理論とPython実装 - LSTMの後継として登場したAttention機構の解説。
- ARIMAによる時系列予測 - 比較対象として古典的手法も合わせて学びましょう。
参考
- Hochreiter, S., & Schmidhuber, J. (1997). Long Short-Term Memory. Neural Computation, 9(8), 1735–1780.
- PyTorch Documentation: torch.nn.LSTM
関連ツール
- DevToolBox - 開発者向け無料ツール集 - JSON整形、正規表現テスターなど85種類以上の開発者向けツール
- CalcBox - 暮らしの計算ツール - 統計計算、複利計算など61種類以上の計算ツール