はじめに
時系列データの予測において、 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}\]通常のニューラルネットワークの誤差逆伝播 は「層を深さ方向にさかのぼる」連鎖律でしたが、RNNのバックプロパゲーション・スルー・タイム(BPTT: Backpropagation Through Time)は同一の重み行列 \(W_h\) を使って時間方向にさかのぼる連鎖律です。ここでは、この時間方向の再帰に特有の勾配消失を数式で証明します。
BPTTにおける勾配の連鎖律
式(1)より、隣接する隠れ状態同士のヤコビアンは次のようになります。
\[ \frac{\partial h_t}{\partial h_{t-1}} = \text{diag}\bigl(\tanh'(z_t)\bigr) \, W_h \] \[ \tag{2} \]ここで \(z_t = W_h h_{t-1} + W_x x_t + b\) です(\(\tanh\) は要素ごとの関数なので、そのヤコビアンは対角行列になります。 通常のニューラルネットワークの逆伝播 で示したのと同じ理由です)。
時刻 \(T\) における損失 \(L_T\) を、\(k\) ステップ過去の隠れ状態 \(h_{T-k}\) で微分すると、連鎖律を \(k\) 回繰り返し適用することで次式を得ます。
\[ \frac{\partial L_T}{\partial h_{T-k}} = \frac{\partial L_T}{\partial h_T} \prod_{i=T-k+1}^{T} \frac{\partial h_i}{\partial h_{i-1}} = \frac{\partial L_T}{\partial h_T} \prod_{i=T-k+1}^{T} \text{diag}\bigl(\tanh'(z_i)\bigr) \, W_h \] \[ \tag{3} \]ここに、\(k\) 個の「対角行列 \(\times\) \(W_h\) 」の積が現れます。通常のニューラルネットワークの深さ方向の勾配消失(層ごとに異なる重み行列が1回ずつ掛かる)と違い、ここでは同一の \(W_h\) が \(k\) 回繰り返し掛かる点が本質的に異なります。
指数的減衰の証明
\(\tanh'(z) = 1-\tanh^2(z) \in (0,1]\) なので、\(\|\text{diag}(\tanh'(z_i))\|_2 \le 1\) が成り立ちます。作用素ノルムの劣乗法性(\(\|AB\|_2 \le \|A\|_2 \|B\|_2\) )を式(3)に繰り返し適用すると、
\[ \left\| \frac{\partial h_T}{\partial h_{T-k}} \right\|_2 \le \prod_{i=T-k+1}^{T} \|\text{diag}(\tanh'(z_i))\|_2 \, \|W_h\|_2 \le \|W_h\|_2^{\,k} \] \[ \tag{4} \]を得ます(\(\|W_h\|_2\) は \(W_h\) の最大特異値/作用素ノルム。\(W_h\) が対称行列であれば固有値の絶対値の最大値に一致します)。したがって \(\|W_h\|_2 < 1\) ならば、式(4)の上界は \(k \to \infty\) で指数的に0へ収束します。これが勾配消失の数学的な正体です。逆に \(\|W_h\|_2 > 1\) の場合は勾配爆発が起こり得ますが、\(\tanh\) の飽和により \(|z_i|\) が大きくなるほど \(\tanh'(z_i)\) が急速に0へ近づくため、実務上は勾配消失の方が支配的になりやすいことが知られています(Bengio et al., 1994; Pascanu et al., 2013)。
損失を全時刻について総和した完全な勾配は、式(3)を \(t=k,\ldots,T\) について足し合わせて
\[ \frac{\partial L}{\partial h_k} = \sum_{t=k}^{T} \frac{\partial L_t}{\partial h_t} \prod_{i=k+1}^{t} \text{diag}\bigl(\tanh'(z_i)\bigr) \, W_h \] \[ \tag{5} \]となります。式(4)の上界より、\(t-k\) が大きい(=時間的に遠い)項ほど指数的に小さくなるため、通常のRNNは近い過去の情報しか実質的に学習できないという構造的な限界を持ちます。
LSTMはセル状態 \(c_t\) という「記憶ライン」を導入し、この掛け算だけの経路(式(3)の \(\prod \text{diag}(\tanh') W_h\) )とは別に、加算だけで情報が伝わる経路を作ることでこの問題を緩和します。仕組みは後述の「LSTMが勾配消失を緩和する仕組み」で具体的に導出します。
LSTMのアーキテクチャ
LSTMは1つのセルに3つのゲートを持ちます。入力は現在の入力 \(x_t\) と前時刻の隠れ状態 \(h_{t-1}\) です。
忘却ゲート (Forget Gate)
過去のセル状態のどの情報を忘れるかを決定します。
\[f_t = \sigma(W_f [h_{t-1}, x_t] + b_f) \tag{6}\]\(\sigma\) はシグモイド関数で、出力は \([0, 1]\) の範囲です。0 に近いほど「忘れる」、1 に近いほど「保持する」を意味します。
入力ゲート (Input Gate)
新しい情報をセル状態にどれだけ書き込むかを制御します。
\[i_t = \sigma(W_i [h_{t-1}, x_t] + b_i) \tag{7}\] \[\tilde{c}_t = \tanh(W_c [h_{t-1}, x_t] + b_c) \tag{8}\]セル状態の更新
忘却ゲートと入力ゲートを組み合わせてセル状態を更新します。
\[c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t \tag{9}\]ここで \(\odot\) はアダマール積(要素ごとの積)です。この加算形の更新式により、勾配が長期間にわたって流れやすくなります(仕組みは後述)。
出力ゲート (Output Gate)
セル状態から何を出力するかを決定します。
\[o_t = \sigma(W_o [h_{t-1}, x_t] + b_o) \tag{10}\] \[h_t = o_t \odot \tanh(c_t) \tag{11}\]パラメータ数
入力次元を \(d_x\) 、隠れ層次元を \(d_h\) とすると、1つのLSTMセルのパラメータ数は次のようになります。
\[4 \times (d_h \times (d_x + d_h) + d_h) \tag{12}\]4つの行列(\(W_f, W_i, W_c, W_o\) )それぞれに \(d_h \times (d_x + d_h)\) の重みと \(d_h\) のバイアスがあります。
なぜ3つのゲートが必要か
3つのゲートはそれぞれ独立の役割を持ち、どれか1つを外すと具体的な失敗モードが生じます。
- 忘却ゲート(\(f_t\) )を外す(\(f_t \equiv 1\) に固定)と何が起きるか:セル状態の更新は \(c_t = c_{t-1} + i_t \odot \tilde{c}_t\) という単純な累積和になります。実は1997年の元祖LSTM(Hochreiter & Schmidhuber, 1997)には忘却ゲートが存在せず、まさにこの形でした。この構成は、入力が明示的に区切られていない連続的なストリームに対しては \(c_t\) が際限なく増大し、ネットワークが破綻することが Gers, Schmidhuber & Cummins (2000) の “Learning to Forget” で報告されています。忘却ゲートは、セル状態を適切なタイミングでリセットする(過去の情報を能動的に消去する)ための機構です。
- 入力ゲート(\(i_t\) )を外す(\(i_t \equiv 1\) に固定)と何が起きるか:新しい候補値 \(\tilde{c}_t\) が無条件にセル状態へ書き込まれます。ノイズや無関係な入力が来ても素通りでセル状態を汚染するため、どの情報が「記憶する価値があるか」をフィルタリングできなくなります。
- 出力ゲート(\(o_t\) )を外す(\(o_t \equiv 1\) に固定、\(h_t = \tanh(c_t)\) )と何が起きるか:セル状態の内容が毎時刻そのまま隠れ状態として露出します。長期記憶として保持しておきたい情報と、今この瞬間の出力に使いたい情報を分離できず、「内部に保存はするが今は出力しない」という制御ができなくなります。
この直感は経験的にも裏付けられています。Greff et al. (2015) の “LSTM: A Search Space Odyssey” は、8種のLSTM亜種を3つのベンチマークタスク・合計5,400通りの設定で比較する大規模研究で、忘却ゲートと出力の活性化関数(\(\tanh\) )を取り除くと全タスクで性能が大きく悪化する一方、入力ゲートと忘却ゲートを結合する(\(i_t = 1-f_t\) とする、GRUに近い構成)などの簡略化は性能にほとんど影響しないと報告しています。つまり3つのゲートは等しく重要というより、「忘却」と「出力の絞り込み」が特に本質的というのが実証的な結論です。
LSTMが勾配消失を緩和する仕組み
セル状態の直接偏微分
式(9)のセル状態更新 \(c_t = f_t \odot c_{t-1} + i_t \odot \tilde{c}_t\) を、ゲート値 \(f_t, i_t, \tilde{c}_t\) を(この瞬間は)定数とみなして \(c_{t-1}\) で直接偏微分すると、
\[ \left. \frac{\partial c_t}{\partial c_{t-1}} \right|_{\text{direct}} = \text{diag}(f_t) \] \[ \tag{13} \]を得ます。式(2)の通常RNNのヤコビアン \(\text{diag}(\tanh'(z_t)) W_h\) と比較すると、重み行列 \(W_h\) による線形変換も \(\tanh'\) による追加の減衰もない、対角行列 \(\text{diag}(f_t)\) だけの単純な形になっていることがわかります。\(f_t \to 1\) (忘却ゲートが開き切っている状態)のとき、この直接経路は恒等写像 \(I\) に近づき、勾配は一切減衰せずにそのまま伝わります。これがHochreiter & Schmidhuber (1997) の言う “Constant Error Carousel”(一定誤差の回転木馬)の数学的実体です。
厳密には:間接経路も存在する
ただし \(c_t\) は \(h_{t-1}\) を経由して間接的にも \(c_{t-1}\) に依存します(\(h_{t-1} = o_{t-1} \odot \tanh(c_{t-1})\) であり、\(f_t, i_t, \tilde{c}_t\) はいずれも \(h_{t-1}\) の関数だからです)。厳密な全微分は
\[ \frac{d c_t}{d c_{t-1}} = \underbrace{\text{diag}(f_t)}_{\text{直接経路(式13)}} + \underbrace{\frac{\partial c_t}{\partial h_{t-1}} \cdot \text{diag}(o_{t-1}) \, \text{diag}\bigl(\tanh'(c_{t-1})\bigr)}_{\text{間接経路(}h_{t-1}\text{経由)}} \] \[ \tag{14} \]となり、\(\partial c_t/\partial h_{t-1}\) の中には \(f_t, i_t, \tilde{c}_t\) それぞれのシグモイド/\(\tanh\) の微分と重み行列 \(W_f, W_i, W_c\) の積が現れます。つまり間接経路は通常RNNと同型の「掛け算だけの経路」であり、それ自体は勾配消失し得ます。LSTMが勾配消失を"解決する"のではなく"緩和する"と言われる所以はここにあります。
しかし式(13)の直接経路は間接経路とは独立に存在し、\(k\) ステップ分の直接経路だけを取り出すと
\[ \frac{\partial c_t}{\partial c_{t-k}} \supseteq \prod_{i=t-k+1}^{t} \text{diag}(f_i) \] \[ \tag{15} \]という項が常に加算的に含まれます。式(4)の通常RNNの上界 \(\|W_h\|_2^{\,k}\) と異なり、この項は重み行列を一切含まず、\(f_i \approx 1\) である限り \(k\) が大きくなっても顕著には減衰しません。したがって、忘却ゲートが十分に1に近い値で学習されている(あるいは初期化されている)区間では、セル状態を通じた勾配が長期にわたって「加算的に」保存されます。
重要な注意点:この緩和効果は \(f_t \approx 1\)
という条件に依存します。\(f_t\)
は学習される値であり、何も手を打たなければ初期化時点では \(f_t \approx 0.5\)
程度(後述の数値実験を参照)にしかならず、式(15)の直接経路も \(0.5^k\)
のオーダーで減衰してしまいます。この問題への実務的な対処として、Jozefowicz et al. (2015) は忘却ゲートのバイアス \(b_f\)
を1程度の正の値で初期化することを推奨しており、多くのLSTM実装(PyTorchの nn.LSTM を含む)でこの初期化トリックがしばしば手動で適用されます。次節でこの効果を実測します。
勾配消失の実験的検証:RNN vs LSTM
理論的な緩和効果が実際にどの程度のオーダーで効くのかを、PyTorchで直接測定して検証します。系列長100・隠れ次元32の(1) 通常RNN(tanh)、(2) デフォルト初期化のLSTM、(3) 忘却ゲートバイアスを \(+3\) で初期化したLSTM、の3設定について、最終時刻の損失 \(L = \|h_{100}\|^2\) から各時刻の隠れ状態 \(h_t\) への勾配ノルム \(\|\partial L/\partial h_t\|\) を計測しました。
import numpy as np
import torch
import torch.nn as nn
SEQ_LEN = 100
INPUT_SIZE = 1
HIDDEN_SIZE = 32
N_TRIALS = 5 # 複数シードで平均し、初期値依存のばらつきを均す
def run_rnn_trial(seed):
torch.manual_seed(seed)
rnn = nn.RNN(input_size=INPUT_SIZE, hidden_size=HIDDEN_SIZE,
nonlinearity="tanh", batch_first=True)
Wx, Wh, bx, bh = rnn.weight_ih_l0, rnn.weight_hh_l0, rnn.bias_ih_l0, rnn.bias_hh_l0
x = torch.randn(SEQ_LEN, INPUT_SIZE)
h_list = [torch.zeros(1, HIDDEN_SIZE, requires_grad=True)]
for t in range(SEQ_LEN):
h_prev = h_list[-1]
h_prev.retain_grad()
z = x[t:t+1] @ Wx.T + bx + h_prev @ Wh.T + bh
h_list.append(torch.tanh(z))
h_list[-1].retain_grad()
loss = h_list[-1].pow(2).sum()
loss.backward()
return np.array([0.0 if h.grad is None else h.grad.norm().item() for h in h_list])
def run_lstm_trial(seed, forget_bias_init=None):
torch.manual_seed(seed)
lstm = nn.LSTM(input_size=INPUT_SIZE, hidden_size=HIDDEN_SIZE, batch_first=True)
H = HIDDEN_SIZE
Wi, Wh_l, bi, bh_l = lstm.weight_ih_l0, lstm.weight_hh_l0, lstm.bias_ih_l0, lstm.bias_hh_l0
if forget_bias_init is not None:
with torch.no_grad():
bi[H:2*H] = forget_bias_init / 2
bh_l[H:2*H] = forget_bias_init / 2 # PyTorchの [i, f, g, o] 順
x = torch.randn(SEQ_LEN, INPUT_SIZE)
def cell(x_t, h_prev, c_prev):
gates = x_t @ Wi.T + bi + h_prev @ Wh_l.T + bh_l
i_g = torch.sigmoid(gates[:, 0:H])
f_g = torch.sigmoid(gates[:, H:2*H])
g_g = torch.tanh(gates[:, 2*H:3*H])
o_g = torch.sigmoid(gates[:, 3*H:4*H])
c_new = f_g * c_prev + i_g * g_g
h_new = o_g * torch.tanh(c_new)
return h_new, c_new
h_list = [torch.zeros(1, H, requires_grad=True)]
c_list = [torch.zeros(1, H, requires_grad=True)]
for t in range(SEQ_LEN):
h_prev, c_prev = h_list[-1], c_list[-1]
h_prev.retain_grad()
c_prev.retain_grad()
h_new, c_new = cell(x[t:t+1], h_prev, c_prev)
h_list.append(h_new)
c_list.append(c_new)
h_list[-1].retain_grad()
loss = h_list[-1].pow(2).sum()
loss.backward()
return np.array([0.0 if h.grad is None else h.grad.norm().item() for h in h_list])
rnn_curves = np.stack([run_rnn_trial(s) for s in range(N_TRIALS)])
lstm_default_curves = np.stack([run_lstm_trial(s, forget_bias_init=None) for s in range(N_TRIALS)])
lstm_biased_curves = np.stack([run_lstm_trial(s, forget_bias_init=3.0) for s in range(N_TRIALS)])
rnn_mean = rnn_curves.mean(axis=0)
lstm_default_mean = lstm_default_curves.mean(axis=0)
lstm_biased_mean = lstm_biased_curves.mean(axis=0)
実行結果(5シード平均、\(t=100\) が損失に最も近い現在側、\(t=0\) が最も過去)は次の通りです。
| \(t\) | 通常RNN (tanh) | LSTM(デフォルト初期化) | LSTM(忘却バイアス \(+3\) ) |
|---|---|---|---|
| 0 | \(5.68 \times 10^{-20}\) | \(9.47 \times 10^{-21}\) | \(3.01 \times 10^{-1}\) |
| 30 | \(1.97 \times 10^{-14}\) | \(2.70 \times 10^{-15}\) | \(1.08 \times 10^{-1}\) |
| 50 | \(1.25 \times 10^{-10}\) | \(1.50 \times 10^{-11}\) | \(1.22 \times 10^{-1}\) |
| 70 | \(7.15 \times 10^{-7}\) | \(9.79 \times 10^{-8}\) | \(1.26 \times 10^{-1}\) |
| 90 | \(9.36 \times 10^{-3}\) | \(9.14 \times 10^{-4}\) | \(1.41 \times 10^{-1}\) |
| 100 | \(2.57\) | \(8.22 \times 10^{-1}\) | \(4.58\) |
\(t=0\) と \(t=100\) の比(過去100ステップにわたる勾配保存率の目安)を取ると、通常RNNは \(2.22 \times 10^{-20}\) 、デフォルト初期化のLSTMは \(1.15 \times 10^{-20}\) と通常RNNとほぼ同じオーダーで消失しているのに対し、忘却バイアスを \(+3\) で初期化したLSTMは \(6.57 \times 10^{-2}\) と、実に18桁近く勾配が保存されています。
この結果は式(13)〜(15)の理論と正確に対応します。\(\sigma(0)=0.5\) なので、デフォルト初期化(バイアス \(\approx 0\) )では忘却ゲートが平均 \(f_t \approx 0.5\) 程度にしかならず、式(15)の直接経路も \(0.5^{100}\) のオーダーで消失し、通常RNNと大差ない結果になります。一方 \(\sigma(3.0) \approx 0.953\) なので、忘却バイアスを \(+3\) で初期化すると \(f_t \approx 0.95\) となり、\(0.95^{100} \approx 6 \times 10^{-3}\) 程度に留まる直接経路が支配的になって勾配が保存されます。「LSTMだから自動的に勾配消失が解決する」わけではなく、忘却ゲートが実際に1に近い値を取ってはじめて緩和が効く、という点が、この節で強調したい最も重要な注意点です。

時系列予測への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()
このコードを実際に実行すると、テストデータに対して RMSE \(\approx 0.2115\) (乱数シード42、正規化前のスケール)が得られました。
ARIMAとLSTMの比較
| 観点 | ARIMA | LSTM |
|---|---|---|
| 前提条件 | 定常性(差分で対応) | なし |
| 非線形モデリング | 不可 | 可能 |
| 長期依存関係 | 弱い(低次ARIMAの場合) | セル状態で保持 |
| パラメータ数 | 少(p, d, q 程度) | 多(数千〜数万) |
| 解釈可能性 | 高い(係数の意味が明確) | 低い(ブラックボックス) |
| 学習データ量 | 少量でも可 | 多量が必要 |
| 学習時間 | 速い | 遅い(GPUで高速化可) |
| 推論時間 | 速い | 中程度 |
| 過学習リスク | 低い | 高い(Dropoutで対策) |
選択の指針:
- データ量が少ない・線形な季節性がある → ARIMA/SARIMA
- 非線形・複雑なパターン・多変量 → LSTM
- リアルタイム推論が必要 → 軽量なLSTMまたはARIMA
実測:フェアな比較条件で予測精度を検証する
上記のLSTMと同じ合成データ(正弦波+ノイズ、SEQ_LEN=30、80/20分割)に対して、
ARIMA
(2,1,0) を実際にフィットし比較しました。
from statsmodels.tsa.arima.model import ARIMA
raw_train = data[:split + SEQ_LEN]
raw_test = data[split + SEQ_LEN:]
# (a) 多段先(dynamic)予測:テスト区間の真値を一切使わない純外挿
model = ARIMA(raw_train, order=(2, 1, 0))
fit = model.fit()
multi_step_pred = fit.forecast(steps=len(raw_test))
rmse_multistep = np.sqrt(np.mean((multi_step_pred - raw_test) ** 2))
# (b) 1step先のwalk-forward予測:直前までの真値を使い1stepだけ予測
# (LSTMのSeq2Oneと同条件:直前SEQ_LEN点の真値が既知)
history_fit = fit
one_step_preds = []
for i in range(len(raw_test)):
pred = history_fit.forecast(steps=1)[0]
one_step_preds.append(pred)
history_fit = history_fit.append([raw_test[i]], refit=False)
rmse_onestep = np.sqrt(np.mean((np.array(one_step_preds) - raw_test) ** 2))
結果は次の通りです。
| 手法 | RMSE | 条件 |
|---|---|---|
| LSTM(Seq2One) | \(0.2115\) | 直前30点の真値が既知、1点先を予測 |
| ARIMA(2,1,0) 多段先(dynamic)予測 | \(1.2796\) | 訓練データ以降、テストの真値を一切使わない純外挿 |
| ARIMA(2,1,0) 1step walk-forward予測 | \(0.2245\) | 直前までの真値が既知、1点先を予測(LSTMと同条件) |
この比較には重要な注意点があります。単純に「LSTM RMSE \(0.21\)
vs ARIMA多段先 RMSE \(1.28\)
」だけを見ると、LSTMがARIMAを6倍近く上回るように見えますが、これは条件がフェアではありません。LSTMのSeq2One構成は毎回直前30点の真値を入力として1点先だけを予測しているのに対し、ARIMAのforecast(steps=len(raw_test))は訓練データ以降のテスト区間について真値を一切参照せずに長期外挿しているため、周期的な系列では位相がずれて誤差が蓄積しやすくなります。同じ「直前まで真値既知・1点先予測」という条件でARIMAを実行する(walk-forward、上記コードの(b))と、RMSEは \(0.2245\)
まで改善し、LSTMの \(0.2115\)
とほぼ互角になります。単純な正弦波1系列の1ステップ予測というタスクでは、LSTMの非線形表現力の優位性は限定的であり、ARIMAで十分な精度が出ることがわかります。LSTMの真価は、非線形性が強い・多変量である・長期依存が本質的に重要である、といった状況でこそ発揮されます。
多変量時系列への拡張
複数の特徴量を使う場合は input_size を変更するだけです。
# 例: 温度・湿度・気圧の3変数から翌時刻の温度を予測
model = LSTMForecaster(input_size=3, hidden_size=128, num_layers=2)
# X_train_t: [batch, seq_len, 3]
実測:多変量化による予測精度の変化
温度・湿度・気圧を模した3変数の合成時系列(それぞれ異なる周期・位相のサイン波にノイズを加えたもの、系列長1000点)を生成し、(a) 3変数すべてを入力として翌時刻の温度を予測するLSTMと、(b) 温度の単変量のみを入力とするLSTMを、同一の学習条件(hidden_size=64, num_layers=2, epochs=50、LSTMForecaster クラスは前掲の「LSTMモデル定義」節と同じもの)で比較しました。
np.random.seed(7)
n = 1000
time = np.linspace(0, 8 * np.pi, n)
temp = 20 + 5 * np.sin(time) + 0.3 * np.random.randn(n)
humidity = 60 - 10 * np.sin(time + 0.5) + 0.5 * np.random.randn(n)
pressure = 1013 + 3 * np.sin(time * 0.5) + 0.2 * np.random.randn(n)
mv_data = np.stack([temp, humidity, pressure], axis=1) # [n, 3]
mv_mean, mv_std = mv_data.mean(axis=0), mv_data.std(axis=0)
mv_norm = (mv_data - mv_mean) / mv_std
def create_mv_sequences(data, seq_len):
X, y = [], []
for i in range(len(data) - seq_len):
X.append(data[i:i + seq_len, :]) # [seq_len, 3]
y.append(data[i + seq_len, 0]) # 翌時刻の温度(列0)のみ予測
return np.array(X), np.array(y)
MV_SEQ_LEN = 30
Xm, ym = create_mv_sequences(mv_norm, MV_SEQ_LEN)
mv_split = int(len(Xm) * 0.8)
Xm_train_t = torch.FloatTensor(Xm[:mv_split])
Xm_test_t = torch.FloatTensor(Xm[mv_split:])
ym_train_t = torch.FloatTensor(ym[:mv_split]).unsqueeze(-1)
ym_test_t = torch.FloatTensor(ym[mv_split:]).unsqueeze(-1)
mv_loader = DataLoader(TensorDataset(Xm_train_t, ym_train_t), batch_size=32, shuffle=True)
# (a) 3変数すべてを入力(LSTMForecaster は「LSTMモデル定義」節のクラスを再利用)
mv_model = LSTMForecaster(input_size=3, hidden_size=64, num_layers=2)
mv_optimizer = torch.optim.Adam(mv_model.parameters(), lr=1e-3)
for epoch in range(EPOCHS):
mv_model.train()
for Xb, yb in mv_loader:
mv_optimizer.zero_grad()
loss = criterion(mv_model(Xb), yb)
loss.backward()
nn.utils.clip_grad_norm_(mv_model.parameters(), max_norm=1.0)
mv_optimizer.step()
mv_model.eval()
with torch.no_grad():
mv_pred = mv_model(Xm_test_t).numpy().squeeze() * mv_std[0] + mv_mean[0]
mv_true = ym[mv_split:] * mv_std[0] + mv_mean[0]
rmse_mv = np.sqrt(np.mean((mv_pred - mv_true) ** 2))
# (b) 比較対象: 温度の単変量のみを入力(同じ温度系列、同じ分割・学習条件)
temp_norm = (temp - temp.mean()) / temp.std()
Xu, yu = create_sequences(temp_norm, MV_SEQ_LEN) # create_sequences は「データ準備」節と同じ関数
Xu_train_t = torch.FloatTensor(Xu[:mv_split]).unsqueeze(-1)
Xu_test_t = torch.FloatTensor(Xu[mv_split:]).unsqueeze(-1)
yu_train_t = torch.FloatTensor(yu[:mv_split]).unsqueeze(-1)
u_loader = DataLoader(TensorDataset(Xu_train_t, yu_train_t), batch_size=32, shuffle=True)
u_model = LSTMForecaster(input_size=1, hidden_size=64, num_layers=2)
u_optimizer = torch.optim.Adam(u_model.parameters(), lr=1e-3)
for epoch in range(EPOCHS):
u_model.train()
for Xb, yb in u_loader:
u_optimizer.zero_grad()
loss = criterion(u_model(Xb), yb)
loss.backward()
nn.utils.clip_grad_norm_(u_model.parameters(), max_norm=1.0)
u_optimizer.step()
u_model.eval()
with torch.no_grad():
u_pred = u_model(Xu_test_t).numpy().squeeze() * temp.std() + temp.mean()
u_true = yu[mv_split:] * temp.std() + temp.mean()
rmse_uni = np.sqrt(np.mean((u_pred - u_true) ** 2))
print(f"多変量LSTM RMSE: {rmse_mv:.4f} 単変量LSTM RMSE: {rmse_uni:.4f}")
結果、3変数入力のLSTMはRMSE \(0.2948\) 、温度単変量のみのLSTMはRMSE \(0.3188\) となり、多変量化により誤差が 約7.5%改善しました。湿度・気圧という補助変数が温度の予測に有効な追加情報を与えていることが確認できます(ただし改善幅は入力変数間の相関の強さに依存するため、無関係な変数を追加しても精度は改善しません)。
近年の研究動向(2023年以降):LSTMの現在地
LSTMが時系列予測の主役だった時代は過ぎ、2023年前後から研究の中心はTransformer系・線形モデル系・状態空間モデル系へ大きくシフトしています。ここでは代表的な流れを簡潔に紹介し、LSTMの現在の立ち位置を正直に整理します。
- DLinear(線形モデルの逆襲):Zeng et al. (2023) の “Are Transformers Effective for Time Series Forecasting?"(AAAI 2023)は、時系列を トレンド成分と季節成分に分解してそれぞれ単純な線形層で予測する DLinear が、当時のTransformer系SOTAモデルの多くを上回ることを示し、大きな議論を呼びました。この論文は「Self-Attentionの順序不変性(permutation-invariance)は時系列の時間順序という強い帰納バイアスと相性が悪いのではないか」という問いを投げかけ、複雑なアーキテクチャの前にまず単純な線形ベースラインで殴ってみることの重要性を再認識させました。
- PatchTST:Nie et al. (2023, ICLR) の “A Time Series is Worth 64 Words” は、系列をパッチ(部分区間)に分割してTransformerのトークンとして扱う手法で、チャネル独立(各変量を独立に処理)と組み合わせることで長期予測の精度を大きく改善しました。
- iTransformer:Liu et al. (2024, ICLR) は、時間方向ではなく変量方向にトークンを取る「反転Transformer」を提案し、多変量時系列における変量間の相関を直接Attentionで捉える設計で高い性能を報告しています。
- TimesNet:Wu et al. (2023, ICLR) は、1次元の時系列を周期性に基づいて2次元テンソルに折りたたみ、画像処理由来のInception型ブロックで多周期パターンを同時に扱う手法です。
- Mambaベースの状態空間モデル:Gu & Dao (2023-2024) の Mamba(選択的状態空間モデル、Selective State Space Model)は、RNNのような逐次的な状態更新の帰納バイアスを保ちながら、ハードウェア対応の並列アルゴリズムでTransformer並みの学習効率を実現するアーキテクチャです。2024年には MambaTS や TimeMachine など、時系列予測へのMamba応用も複数提案されています。線形時間・線形メモリで長系列を扱える点はLSTMの系譜に連なる発想と言えます。
これらの動向は Transformerによる時系列予測 でより詳しく扱っています。LSTMの現在の位置づけを正直にまとめると、大規模・多変量・長期の公開ベンチマークでは上記の新しいアーキテクチャに軒並み精度で見劣りするようになってきているものの、(1) パラメータ数・計算量が小さく学習が速い軽量なベースラインとして依然有用、(2) ゲートの開閉という形で挙動をある程度追跡でき、Transformerの多頭Attentionよりは解釈の足がかりがある、(3) 本記事の実測で見た通り、単変量・短中期の予測ではARIMAのような古典的手法と大差がつかないことも多く、過度に複雑なモデルを避けるべき場面もある、(4) ストリーミング推論(1ステップごとに定数時間・定数メモリで状態更新でき、Transformerのように系列全体を保持し直す必要がない)に向く、という理由から、「まず試す軽量な非線形ベースライン」としての実用価値は今なお失われていません。
関連記事
- 時系列予測の基礎:ARIMAモデルの理論とPython実装 - 線形時系列モデルの古典的手法。LSTMと比較すると解釈性の高さが特徴です。
- PACF・ARモデル次数同定 - 古典的 AR モデルの次数を PACF で同定する手法。LSTM の lookback(入力系列長)を決める際の統計的な目安にもなります。
- 遺伝的アルゴリズムによるニューラルネットワーク学習のPython実装 - 勾配を使わずにニューラルネットワークを最適化するアプローチ。
- 確率的勾配降下法とAdamの理論とPython実装 - LSTMの学習に使うAdamオプティマイザの詳細。
- 時系列データの異常検知:統計的手法からカルマンフィルタまで - LSTMを使った異常検知にも応用できる手法群。
- Self-Attentionの理論とPython実装 - LSTMの後継として登場したAttention機構の解説。
- Transformerによる時系列予測 - PatchTST・Informer・Autoformerなど、LSTMの後継となる長期予測モデルの系譜を深掘りしています。
- ARIMAによる時系列予測 - 比較対象として古典的手法も合わせて学びましょう。
- 機械学習による時系列予測・分類・異常検知ハブ - LSTM を含む 7 手法(k-means/GMM・RF/GBDT・LSTM・カルマン・Isolation Forest)の使い分けを 3 軸 × 9 シナリオで整理したハブ記事。
- アンサンブル学習(Random Forest / Gradient Boosting) - GBDT + ラグ特徴量と LSTM の予測精度比較は時系列モデル選定の基本軸。短中期予測では GBDT がしばしば LSTM を上回ります。
- ベイズ最適化の基礎とPython実装 - LSTM の hidden_size / layers / lookback / learning_rate を 20〜50 試行で自動探索。Optuna の TPE / GP-EI で実用域に到達できます。
参考
- Hochreiter, S., & Schmidhuber, J. (1997). Long Short-Term Memory. Neural Computation, 9(8), 1735–1780.
- Gers, F. A., Schmidhuber, J., & Cummins, F. (2000). Learning to Forget: Continual Prediction with LSTM. Neural Computation, 12(10), 2451–2471.
- Greff, K., Srivastava, R. K., Koutník, J., Steunebrink, B. R., & Schmidhuber, J. (2015). LSTM: A Search Space Odyssey. arXiv:1503.04069.
- Jozefowicz, R., Zaremba, W., & Sutskever, I. (2015). An Empirical Exploration of Recurrent Network Architectures. ICML 2015.
- Bengio, Y., Simard, P., & Frasconi, P. (1994). Learning Long-Term Dependencies with Gradient Descent is Difficult. IEEE Transactions on Neural Networks, 5(2), 157–166.
- Pascanu, R., Mikolov, T., & Bengio, Y. (2013). On the Difficulty of Training Recurrent Neural Networks. ICML 2013.
- Zeng, A., Chen, M., Zhang, L., & Xu, Q. (2023). Are Transformers Effective for Time Series Forecasting? AAAI 2023.
- Nie, Y., Nguyen, N. H., Sinthong, P., & Kalagnanam, J. (2023). A Time Series is Worth 64 Words: Long-term Forecasting with Transformers. ICLR 2023.
- Liu, Y., Hu, T., Zhang, H., Wu, H., Wang, S., Ma, L., & Long, M. (2024). iTransformer: Inverted Transformers Are Effective for Time Series Forecasting. ICLR 2024.
- Wu, H., Hu, T., Liu, Y., Zhou, H., Wang, J., & Long, M. (2023). TimesNet: Temporal 2D-Variation Modeling for General Time Series Analysis. ICLR 2023.
- Gu, A., & Dao, T. (2023). Mamba: Linear-Time Sequence Modeling with Selective State Spaces. arXiv:2312.00752.
- PyTorch Documentation: torch.nn.LSTM
関連ツール
- DevToolBox - 開発者向け無料ツール集 - JSON整形、正規表現テスターなど85種類以上の開発者向けツール
- CalcBox - 暮らしの計算ツール - 統計計算、複利計算など61種類以上の計算ツール