6.4.4 シーケンスモデリング実践

- 連続した時系列を教師あり学習サンプルに変換する。
- LSTM 入力を
[batch, seq_len, input_size]に保つ。 - 未来情報の漏洩を避けるため、検証データを時間順に分ける。
- LSTM 予測器を学習し、naive baseline と比較する。
- 検証 loss と予測サンプルを読めるようになる。
中心となる流れ
Section titled “中心となる流れ”連続系列sliding window時間順 splitLSTM検証 MSE予測確認
時系列では、基本的にランダム分割を避けます。未来の点が訓練に漏れると、検証が楽観的になりすぎます。
1 分でわかるスライディングウィンドウ
Section titled “1 分でわかるスライディングウィンドウ”window_size = 3 の場合:
series: [1, 2, 3, 4, 5, 6]
X[0] = [1, 2, 3] -> y[0] = 4X[1] = [2, 3, 4] -> y[1] = 5X[2] = [3, 4, 5] -> y[2] = 6このように、連続系列を訓練用の行に変換します。
完全な実験:LSTM 予測
Section titled “完全な実験:LSTM 予測”合成系列は 2 つの波とノイズでできています。まだ小さなデータですが、完全な正弦波より実データに近いです。
import numpy as npimport torchfrom torch import nn
np.random.seed(42)torch.manual_seed(42)
def make_windows(series, window_size): X, y = [], [] for i in range(len(series) - window_size): X.append(series[i : i + window_size]) y.append(series[i + window_size]) X = torch.tensor(np.array(X), dtype=torch.float32).unsqueeze(-1) y = torch.tensor(np.array(y), dtype=torch.float32).unsqueeze(-1) return X, y
t = np.arange(0, 220)series = ( np.sin(t * 0.12) + 0.25 * np.sin(t * 0.03) + np.random.randn(len(t)) * 0.04).astype(np.float32)
window_size = 16X, y = make_windows(series, window_size)
split = int(len(X) * 0.8)X_train, X_val = X[:split], X[split:]y_train, y_val = y[:split], y[split:]
print("window_lab")print("X:", tuple(X.shape), "y:", tuple(y.shape))print("train:", tuple(X_train.shape), "val:", tuple(X_val.shape))
naive_val = ((X_val[:, -1, :] - y_val) ** 2).mean().item()print("naive_val_mse:", round(naive_val, 4))
class LSTMForecaster(nn.Module): def __init__(self, hidden_size=32): super().__init__() self.lstm = nn.LSTM(1, hidden_size, batch_first=True) self.fc = nn.Linear(hidden_size, 1)
def forward(self, x): out, _ = self.lstm(x) return self.fc(out[:, -1, :])
model = LSTMForecaster(32)loss_fn = nn.MSELoss()optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
for epoch in range(1, 121): model.train() pred = model(X_train) loss = loss_fn(pred, y_train)
optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step()
if epoch == 1 or epoch % 30 == 0: model.eval() with torch.no_grad(): val_loss = loss_fn(model(X_val), y_val) print(f"epoch={epoch:03d} train_mse={loss.item():.4f} val_mse={val_loss.item():.4f}")
model.eval()with torch.no_grad(): val_pred = model(X_val) print("first_5_pred:", [round(v, 3) for v in val_pred[:5, 0].tolist()]) print("first_5_true:", [round(v, 3) for v in y_val[:5, 0].tolist()])期待される出力:
window_labX: (204, 16, 1) y: (204, 1)train: (163, 16, 1) val: (41, 16, 1)naive_val_mse: 0.0115epoch=001 train_mse=0.5168 val_mse=0.4633epoch=030 train_mse=0.0049 val_mse=0.0046epoch=060 train_mse=0.0032 val_mse=0.0035epoch=090 train_mse=0.0029 val_mse=0.0032epoch=120 train_mse=0.0028 val_mse=0.0030first_5_pred: [0.323, 0.261, 0.145, -0.025, -0.192]first_5_true: [0.4, 0.213, 0.045, -0.076, -0.128]
| 出力 | 意味 |
|---|---|
X: (204, 16, 1) | 204 個の window、16 time steps、各 step 1 feature |
train: (163, 16, 1) | 最初の 80% の window を訓練に使う |
val: (41, 16, 1) | 後ろの window を検証に使う |
naive_val_mse | baseline:最後に観測した値を次の値として予測 |
val_mse | LSTM の検証誤差 |
first_5_pred vs first_5_true | 方向とスケールの簡易確認 |
この実行では、LSTM は naive baseline を上回っています(0.0030 vs 0.0115)。これは重要です。信頼する前に、モデルはまず単純な baseline に勝つべきです。
系列予測では baseline 比較を保存します。
- ウィンドウサイズ
- 16
- 分割ルール
- 時系列順で最初の80%をtrain、最後の20%をvalidation
- ベースライン
- 単純な直前値予測器
- ベースライン検証 MSE
- 0.0115
- モデル検証 MSE
- 0.0030
- サンプル確認
- first_5_pred が first_5_true の方向とスケールに従う
- 失敗確認対象
- 遅延、横ばい、ピークの見逃し、またはノイジーな予測
なぜ Gradient Clipping を使うのか
Section titled “なぜ Gradient Clipping を使うのか”RNN 系のモデルでは、勾配が大きくなることがあります。この行は全体の勾配 norm を制限します。
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)必ず必要とは限りませんが、系列モデルでは実用的な安全策です。
Notebook で何を描くべきか
Section titled “Notebook で何を描くべきか”Notebook では次を追加します。
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 4))plt.plot(y_val.squeeze(-1).numpy(), label="true")plt.plot(val_pred.squeeze(-1).numpy(), label="pred")plt.legend()plt.grid(True, alpha=0.3)plt.show()見るべき点:
- lag:形は追えているが遅れている。
- flatline:平均値のような平らな予測になっている。
- missed peaks:window が短すぎる、またはモデルが弱い。
- noisy prediction:学習率、データノイズ、過学習の問題。
よくある落とし穴
Section titled “よくある落とし穴”| 落とし穴 | なぜ困るか | 修正 |
|---|---|---|
| train/val をランダム分割する | 未来が訓練に漏れる | 時間順に分割する |
| window が短すぎる | 文脈が足りない | 大きい window_size を試す |
| window が長すぎる | 最適化が難しく、ノイズも増える | 検証 loss で比較する |
| baseline がない | モデルがよく見えても実は trivial かもしれない | 最後値 baseline と比べる |
| MSE だけ見る | 傾向が遅れたり平坦化している可能性がある | 予測曲線を描く |
| 実データをスケーリングしない | 値の範囲が大きく訓練が不安定になる | 訓練統計だけで正規化する |
Toy Series から実プロジェクトへ
Section titled “Toy Series から実プロジェクトへ”実際の系列プロジェクトでは、次が必要になることがあります。
- 各ステップに複数特徴。
- 欠損値処理。
- 訓練データだけに基づく正規化。
- rolling-origin validation。
- GRU、Temporal CNN、Transformer、統計 baseline。
- MSE だけでなく業務指標。
それでも流れは同じです。window を定義し、時間順を守り、baseline と比較し、予測を確認します。
window_sizeを8と32に変える。どちらの検証 MSE が良いか。nn.LSTMをnn.GRUに変える。学習速度や曲線は変わるか。- gradient clipping を外す。学習は安定したままか。
np.cos(t * 0.12)などの 2 つ目の feature を追加する。- 予測値を次の window に戻して使う rolling forecast を実装する。
参考実装と解説
- 大きい window は長い履歴を見られますが、学習は難しくなります。検証 MSE は実行結果で比較し、訓練 MSE だけで判断しません。
- GRU は LSTM よりパラメータが少なく、速いことが多いです。タスクが単純なら曲線は近くなる場合があります。
- gradient clipping を外すと、loss の spike や発散が起きることがあります。長い系列や大きい学習率で特に目立ちます。
- feature を 2 つにすると入力の最後の次元が 2 になります。モデルの
input_sizeも合わせて変更します。 - rolling forecast は自分の予測誤差を次の入力に戻すため、先の時刻ほど drift しやすくなります。実運用に近い検証です。
- Sliding window は連続系列を教師あり学習サンプルに変える。
- 時間ベースの検証は未来情報の漏洩を防ぐ。
- 意味のある評価には naive baseline が必要。
- LSTM 入力は
[batch, seq_len, input_size]。 - 曲線と予測サンプルは、1 つの loss 値では見えない問題を見せてくれる。