Mímisbrunnr知恵の泉

← 時系列分析 一覧

🎓 レベル:標準 | 重要度:B(標準)

📎 前提:ラグ特徴量と木モデル(教師あり回帰への変換・窓特徴) | 数理:再帰型ニューラルネットワークTransformer(機械学習テキスト)

⚙️ 要最新確認:深層時系列(RNN/LSTM/Transformer・N-BEATS・PatchTST・TFT、および TimesFM/Chronos/Moirai 等の基盤モデル)は API・SOTA が高速に動く領域です。本ノートの実行コードは torch/tf を使わず scikit-learn 1.6.1 の MLP で完結させ(本環境に深層フレームワーク未導入)、深層アーキテクチャの詳細は機械学習テキストへリンクします。実装時は各ライブラリの最新版・最新ベンチマークを確認してください。

要点(BLUF)

1. 窓(window)を入力にする系列モデル

ラグ特徴量と木モデル では個別のラグ(yt1,yt12,y_{t-1},y_{t-12},\dots)を特徴に選びました。系列モデルは発想を一歩進め、直近 WW 点をまるごと入力ベクトルにします:

xt=(ytW,ytW+1,,yt1)RWy^t=gθ(xt)\mathbf{x}_t = (y_{t-W},\,y_{t-W+1},\,\dots,\,y_{t-1}) \in \mathbb{R}^{W} \quad\longrightarrow\quad \hat y_t = g_\theta(\mathbf{x}_t)

gθg_\theta にニューラルネットを使えば、窓の中の非線形・交互作用を学べます。最も単純なのは多層パーセプトロン(MLP)——窓を入力層に並べたフィードフォワードNNで、古くは time-delay neural network(TDNN) と呼ばれました。学習は誤差逆伝播+勾配降下(再帰型ニューラルネットワーク のニューラルネット基礎は機械学習テキスト)。

ニューラルネットで決定的に重要なのが入力の標準化です。勾配降下は入力のスケールに敏感で、yy の水準が大きい(例:100 前後)まま渡すと、初期の重み×大きな入力で活性が飽和し、勾配がほぼ消えて学習が進みません。各特徴を平均0・分散1にする StandardScaler訓練データだけで当て(テストにも同じ変換を適用=漏洩防止)、Pipeline に組むのが定石です。

flowchart LR
    Y["原系列 y_t"] --> W["スライディング窓 [y_t-W, ..., y_t-1]"]
    W --> S["StandardScaler(訓練のみで当てる)"]
    S --> NN["MLP(フィードフォワードNN)g_theta"]
    NN --> P["次の値 y_t の予測(時間順ホールドアウトで検証)"]

コード①:窓入力の MLP を 線形・木とホールドアウト比較

非線形(しきい値AR=レジーム切替) +季節の系列で、窓 W=12W=12 を入力に MLPランダムフォレスト線形(Ridge)時間順ホールドアウト(シャッフル禁止)で比べます。MLP と Ridge は StandardScalerPipeline で前置。MLP には訓練残差から予測区間も付けます。

import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib  # 日本語ラベル用
from sklearn.neural_network import MLPRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline

# 真の構造:しきい値AR(レジーム切替=非線形)+ 季節。線形では捉えにくい
rng = np.random.default_rng(0)
n, period = 600, 12
t = np.arange(n)
season = 4*np.sin(2*np.pi*t/period)
nl = np.zeros(n)
for i in range(2, n):
    if nl[i-1] < 0:                       # レジーム1:強い持続(係数 0.9)
        nl[i] = 0.9*nl[i-1] + rng.normal(0, 0.6)
    else:                                  # レジーム2:反転(係数 -0.7)
        nl[i] = -0.7*nl[i-1] + rng.normal(0, 0.6)
y = 30 + season + nl

# スライディング窓:過去 W 点の並びを入力ベクトルに、次の1点を出力に
W = 12
X = np.array([y[i-W:i] for i in range(W, n)])
target = np.array([y[i] for i in range(W, n)])
n_test = 100                              # 時間順ホールドアウト(シャッフル禁止)
Xtr, Xte = X[:-n_test], X[-n_test:]
ytr, yte = target[:-n_test], target[-n_test:]

models = {
    "線形(Ridge)": make_pipeline(StandardScaler(), Ridge(alpha=1.0)),
    "木(RF)": RandomForestRegressor(n_estimators=400, random_state=0),
    "NN(MLP)": make_pipeline(StandardScaler(),
                MLPRegressor(hidden_layer_sizes=(20,), max_iter=4000,
                             alpha=1e-3, random_state=0)),
}
preds = {}
for name, m in models.items():
    m.fit(Xtr, ytr)
    preds[name] = m.predict(Xte)
    rmse = np.sqrt(np.mean((preds[name] - yte)**2))
    print(f"ホールドアウト RMSE  {name:>12} = {rmse:.3f}")

# MLP の予測区間:訓練残差の標準偏差から ±1.96σ(正規近似)
mlp = models["NN(MLP)"]
sigma = (ytr - mlp.predict(Xtr)).std(ddof=1)
pm = preds["NN(MLP)"]
lo, hi = pm - 1.96*sigma, pm + 1.96*sigma
cover = int(np.sum((yte >= lo) & (yte <= hi)))
print(f"MLP 95%予測区間  カバレッジ = {cover}/{n_test}  平均幅 = {np.mean(hi-lo):.2f}")

tt = np.arange(W, n)[-n_test:]
plt.figure(figsize=(9, 4.5))
plt.plot(np.arange(W, n)[-160:], target[-160:], color="0.6", lw=1, label="実測")
plt.plot(tt, pm, color="C1", lw=1.8, label="MLP 予測")
plt.fill_between(tt, lo, hi, color="C1", alpha=0.2, label="MLP 95%予測区間")
plt.xlabel("時点 t"); plt.ylabel("y"); plt.legend()
plt.title("スライディング窓 + MLP(フィードフォワードNN)の予測")
plt.tight_layout(); plt.show()

出力:

ホールドアウト RMSE     線形(Ridge) = 0.650
ホールドアウト RMSE         木(RF) = 0.629
ホールドアウト RMSE       NN(MLP) = 0.693
MLP 95%予測区間  カバレッジ = 95/100  平均幅 = 2.60

出力の意味:3手法はほぼ横一線——木(RF) 0.6290.629、線形 0.6500.650、MLP 0.6930.693。非線形のしきい値AR を仕込んだので木と MLP は非線形を学べますが、データ規模(訓練 500\sim500 点)では線形に対する非線形の上積みはわずかで、むしろ木が僅差で最良でした。MLP は競合はするもののこの規模では最良ではない——「窓にNNを通せば最強」ではないと分かります。MLP の予測区間は訓練残差の ±1.96σ\pm1.96\sigma で作り、カバレッジ 95/10095/10095%95\%)と較正も良好(点予測だけでなく区間を必ず添えるのは時系列の鉄則、予測の評価指標と時系列CV)。

コード②:標準化は必須——なしだと MLP は行き詰まる

同じ系列の水準を 100 にして入力スケールを大きくし、StandardScalerあり/なしで MLP を比べます。標準化なしで勾配法がどれだけ詰まるかを見ます。

import warnings; warnings.simplefilter("ignore")
import numpy as np
from sklearn.neural_network import MLPRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline

# 同じ非線形系列だが水準を 100 にして「入力スケールが大きい」状況にする
rng = np.random.default_rng(0)
n, period = 600, 12
t = np.arange(n)
season = 4*np.sin(2*np.pi*t/period)
nl = np.zeros(n)
for i in range(2, n):
    nl[i] = (0.9*nl[i-1] if nl[i-1] < 0 else -0.7*nl[i-1]) + rng.normal(0, 0.6)
y = 100 + season + nl            # 水準100=入力が大きいまま

W = 12
X = np.array([y[i-W:i] for i in range(W, n)])
target = np.array([y[i] for i in range(W, n)])
Xtr, Xte = X[:-100], X[-100:]
ytr, yte = target[:-100], target[-100:]
rmse = lambda p: np.sqrt(np.mean((p - yte)**2))

# 標準化なし:入力(約100)をそのまま勾配法に渡す
raw = MLPRegressor(hidden_layer_sizes=(20,), max_iter=4000, alpha=1e-3,
                   random_state=0).fit(Xtr, ytr)
# 標準化あり:平均0・分散1にしてから同じMLP
scaled = make_pipeline(StandardScaler(),
            MLPRegressor(hidden_layer_sizes=(20,), max_iter=4000, alpha=1e-3,
                         random_state=0)).fit(Xtr, ytr)

print(f"MLP 標準化なし RMSE = {rmse(raw.predict(Xte)):.3f}  (勾配が進まず行き詰まる)")
print(f"MLP 標準化あり RMSE = {rmse(scaled.predict(Xte)):.3f}")

出力:

MLP 標準化なし RMSE = 3.939  (勾配が進まず行き詰まる)
MLP 標準化あり RMSE = 0.746

出力の意味:標準化なしの MLP は RMSE 3.9393.939行き詰まり(入力が約100と大きく、初期から活性が飽和して勾配が進まない)、標準化ありは 0.7460.7465倍以上の差。木モデル(ラグ特徴量と木モデル)はスケールに鈍感で標準化不要でしたが、勾配ベースのニューラルネットでは標準化が事実上必須です。PipelineStandardScaler を組み、訓練だけで当てて(テストに同じ変換を適用)漏洩を防ぐ——これがニューラル予測の前処理の基本です(特徴量エンジニアリングと前処理、機械学習テキスト)。

2. RNN・LSTM・Transformer・基盤モデル(概念のみ・詳細はML テキストへ)

MLP は窓を「ただの固定長ベクトル」として食べるので、並び順や可変長の長期依存を構造的には扱えません。これを設計で取り込むのが系列特化アーキテクチャです(いずれも数理は機械学習テキストへ)。

いつ深層が要るか:単一・短〜中規模・素直な季節トレンドなら、ARIMA/ETS(ARMA・ARIMAモデルETSモデルと状態空間表現)や木ML(ラグ特徴量と木モデル)で十分なことが多い(本ノートのコード①も MLP が最良ではなかった)。深層が活きるのは、多数の系列を一括学習(global model)・長い依存や複雑な非線形外生変数が豊富データが大規模のとき。小データに深層を当てると過学習しやすく、リーク混入の検査も難しくなります。

3. 数式の直観

⚠️ よくある誤解・落とし穴

関連ノート