🎢

レジームスイッチ:動的ウエイト付けによるモデル選択

2024/12/17に公開

はじめに

本記事はマケデコAdvent Calendar 2024の記事として投稿します。botter_01
さんによる金融時系列における機械学習のオーバーフィット対策 でインスパイアされました。感謝。

さて、本記事では、時系列データの予測タスクにおいて、データ生成過程が時間とともに変化する「レジームスイッチ(Regime Switch)」への対応方法と、その戦略的なウエイト更新によるオンライン学習モデルの適応手法のひとつを解説します。

背景とモチベーション

実世界の時系列データは、しばしば「非定常性」と呼ばれる特徴を持ちます。例えば、金融市場では、ある期間は緩やかなトレンドが続いたと思えば、突然ボラティリティが上昇し価格推移が激変したりします。製造業の工程管理では、正常稼働状態からある日突然機械の劣化や故障の兆候が顕在化することがあります。環境データでも、天候パターンが急激に変化する、あるいは季節性が急に崩れることがありえます。

このような「レジーム」(状態)の変化に直面すると、固定的なモデル(例えば、固定パラメータの線形回帰やARIMAモデルなど)は過去のデータで最適化されたパラメータのままでは通用せず、予測性能が大幅に低下することがあります。レジームスイッチ後にその変化に迅速に適応するには、モデルを再学習する、あるいは別のモデルに切り替えるといった戦略が必要です。

しかし、現実には「レジームが変わった」ときっぱり判断するのは難しい場合が多いです。変化点検知手法などで統計的に検出する試みもありますが、誤検知や検出遅れが問題となります。ここで、より緩やかで汎用的な戦略として、複数のモデルを常に用意し、それらのモデルを「エキスパート」とみなし、オンラインでその重み付けを更新することで、自然に「今もっとも有効なモデル」を浮き上がらせる手法が有効になります。


レジームスイッチとは何か

レジームスイッチ(Regime Switch) とは、時系列データが従来と異なる統計的性質(平均、分散、自己相関、分布特性など)を持つ状態に移行する現象を指します。典型的には下記のような状況を「レジームチェンジ」と呼びます。

  • 平均・トレンドの変化:
    ある期間は増加傾向、ある期間は減少傾向、ある期間は横ばい、といった明確なトレンドの転換。

  • 分散・ボラティリティの変化:
    金融資産価格では平穏な時期と乱高下する時期が交互に現れます。安定状態から急に不安定になる変化は典型的なレジームスイッチの一例です。

  • 相関構造・因果関係の変化:
    ある期間は説明変数との強い線形関係が見られたが、別の期間では全く異なる因果関係や相関関係が有効になったりします。

こうした変化は多くの場合、外部環境の影響や市場参加者の行動変容、技術的要因、季節性の崩壊などによって引き起こされ、実務上の予測モデルにとって避けられない難題となっています。


オンライン学習とレジームスイッチへの適性

オンライン学習(Online Learning) とは、データが逐次的に与えられる状況で、各ステップ毎にモデルを更新しながら予測や意思決定を行う手法です。バッチ学習が全データを一度に読み込んで最適なモデルを求めるのに対して、オンライン学習は新データが到着するたびにモデルを微調整します。

これにより、分布変化(レジームスイッチ)が起こった際にも、モデルが迅速に更新され、現行の状態に適応しやすくなります。特に、

  • 過去データを素早く「忘れる」メカニズム
    オンライン学習では、直近のデータに対してモデルパラメータを大きく更新し、過去のデータの影響を徐々に減らすことができます。これにより、レジームが変化した後に、すぐに最新レジームに合ったパラメータへ適応が可能になります。

  • 複数モデル(エキスパート)の並行運用
    複数のモデルを常に走らせておき、逐次到着するデータに対して各モデルの予測誤差を計算し、良いモデルには重みを大きく、悪いモデルには重みを小さくすることで、最終的な予測は「その時点で最も有効なモデル」に近づきます。これにより明示的な変化点検知なしでレジームスイッチに対応できます。


エキスパート重み付け戦略:理論的背景

複数モデル(エキスパート)を用意し、オンラインでその重みを更新する戦略は、オンライン学習の古典的な研究テーマの一つです。

基本的なアイデアは次のとおりです:

  1. 初期時点では、すべてのエキスパート(モデル)に対して均一な重みを割り当てます
  2. 各時刻において、エキスパートの予測を重み付き平均して最終予測を行います
  3. 実際の値が観測されたら、各エキスパートの予測誤差(損失)を計算します
  4. 損失が小さいエキスパートは「ご褒美」を、損失が大きいエキスパートには「ペナルティ」を与えるような形で重みを更新します。具体的には、悪いエキスパートの重みは指数関数的に減少し、よいエキスパートの重みは相対的に維持または増大します
  5. 全エキスパートの重みを正規化して、合計が1になるようにします

これを繰り返すことで、時間経過とともに最も良いエキスパートに重みが集まりやすくなり、結果として全体的な予測精度を維持あるいは向上させられます。

重要なのは、ここで明示的に「レジームが変化した」と判断してはいない点です。ただ、最近のデータに対して良い予測を出すエキスパートが自然と選ばれていき、その背後でレジームチェンジがあれば、その変化をきっかけに従来良かったモデルの精度が落ち、新しいモデルが相対的に有利になるため、重みがシフトすることになります。


線形モデルによるシンプルな例

ここで、非常に単純化した例を考えます。

  • 時系列データ (x_t, y_t) があるとします。説明変数 x_t は時間とともに変化する単純な入力(例:x_t = t または \sin(t)など)。

  • 初期レジームでは真のモデルが y_t = w^{(1)} x_t + \epsilon_t に従い、途中から突然、別のレジーム y_t = w^{(2)} x_t + \epsilon_t に切り替わるとします。

  • 学習者(オンライン学習モデル)はレジーム変更時刻を知りません。また、真のモデルパラメータももちろん知りません。

  • しかし、学習者は2つの候補モデル(エキスパート)を持っています。たとえば、

    • Expert A: \hat{y}_t = w^{(A)} x_t
    • Expert B: \hat{y}_t = w^{(B)} x_t

    ここで w^{(A)}, w^{(B)} は固定値で、実際の真のパラメータ w^{(1)}, w^{(2)} に近い値をあらかじめ設定しておきます。

  • 前半はExpert Aが真のモデルに近く、後半はExpert Bが真のモデルに近い場合、オンライン学習者は最初はAに重みが集まり、途中でデータの挙動が変わるとAの誤差が増大し、Bに重みが移っていくことが期待されます。

このような簡単なシナリオをPythonコードでデモし、可視化することで、オンライン重み付け戦略がどのようにレジームスイッチに対応するかを明示します。


コード例(Google Colab対応)

以下はGoogle Colabで実行可能なコード例です。
このコードでは、

  • データ生成部: レジームチェンジを含む時系列データ y_t を作成します。
  • 2つのエキスパートモデル(線形モデル): Expert AとExpert Bを定義します。
  • オンライン更新則: 現時点での損失に応じてエキスパート重みを更新します。
  • 可視化: 真の値、予測値、予測誤差、重みの変遷をグラフで表示します。
import numpy as np
import matplotlib.pyplot as plt

# ------------------------------------------------------------
# Example Setup:
# ------------------------------------------------------------
# We will create a synthetic time series where:
#   y_t = w^(1)*x_t for t <= T_change
#   y_t = w^(2)*x_t for t >  T_change
#
# We do not tell our online learner when the regime changes.
# Instead, we provide it with two "expert models":
#   Expert A: y_hat = w^(A)*x
#   Expert B: y_hat = w^(B)*x
#
# The learner updates the weights p_t, q_t over time:
# p_{t+1} = p_t * exp(-eta * loss_A_t)
# q_{t+1} = q_t * exp(-eta * loss_B_t)
# then normalized so that p_{t+1} + q_{t+1} = 1.
#
# When the regime changes, the once-good expert becomes poor, and
# the learner naturally shifts weights to the other expert.
# ------------------------------------------------------------

# Seed for reproducibility
np.random.seed(42)

# ------------------------------------------------------------
# Data Generation
# ------------------------------------------------------------
T = 200                       # total time steps
T_change = 100                # time step at which regime changes
x = np.linspace(0, 4*np.pi, T) # input features
noise_std = 0.5               # noise level

# True regimes:
w_true_before = 0.5  # slope in regime 1
w_true_after = 2.0   # slope in regime 2

# Generate true y:
y = np.zeros(T)
y[:T_change] = w_true_before * x[:T_change] + np.random.randn(T_change)*noise_std
y[T_change:] = w_true_after  * x[T_change:] + np.random.randn(T - T_change)*noise_std

# ------------------------------------------------------------
# Experts Setup
# ------------------------------------------------------------
w_A = 0.4 # Expert A slope (close to the first regime)
w_B = 1.8 # Expert B slope (close to the second regime)

# Initial weights for the experts
p = 0.5
q = 0.5
eta = 0.1

p_history = []
q_history = []
combined_preds = []
expertA_preds = []
expertB_preds = []
errors = []

for t in range(T):
    # Experts' predictions:
    y_hat_A = w_A * x[t]
    y_hat_B = w_B * x[t]
    
    # Combined prediction:
    y_hat_comb = p * y_hat_A + q * y_hat_B
    
    # Compute losses (squared)
    loss_A = (y[t] - y_hat_A)**2
    loss_B = (y[t] - y_hat_B)**2
    
    # Update weights:
    p_new = p * np.exp(-eta * loss_A)
    q_new = q * np.exp(-eta * loss_B)
    total = p_new + q_new
    p = p_new / total
    q = q_new / total
    
    # Store
    p_history.append(p)
    q_history.append(q)
    combined_preds.append(y_hat_comb)
    expertA_preds.append(y_hat_A)
    expertB_preds.append(y_hat_B)
    errors.append((y[t] - y_hat_comb)**2)

# ------------------------------------------------------------
# Visualization
# ------------------------------------------------------------
plt.figure(figsize=(14,10))

# Plot 1: True y and combined predictions
plt.subplot(2,2,1)
plt.plot(y, label='True y', linewidth=2)
plt.plot(combined_preds, label='Combined Prediction', linewidth=2)
plt.axvline(T_change, color='red', linestyle='--', label='Regime change')
plt.title('True data vs. Combined Prediction')
plt.xlabel('Time step')
plt.ylabel('Value')
plt.legend()

# Plot 2: Prediction error over time
plt.subplot(2,2,2)
plt.plot(errors, label='(y - y_hat_comb)^2', color='darkgreen')
plt.title('Prediction Error')
plt.xlabel('Time step')
plt.ylabel('Squared Error')
plt.axvline(T_change, color='red', linestyle='--')
plt.legend()

# Plot 3: Expert weights
plt.subplot(2,2,3)
plt.plot(p_history, label='Weight on Expert A', color='blue')
plt.plot(q_history, label='Weight on Expert B', color='orange')
plt.axvline(T_change, color='red', linestyle='--')
plt.title('Weights Over Time')
plt.xlabel('Time step')
plt.ylabel('Weight')
plt.legend()

# Plot 4: Experts predictions comparison
plt.subplot(2,2,4)
plt.plot(y, label='True y', linewidth=2)
plt.plot(expertA_preds, label='Expert A Prediction', linewidth=1.5, color='blue')
plt.plot(expertB_preds, label='Expert B Prediction', linewidth=1.5, color='orange')
plt.axvline(T_change, color='red', linestyle='--', label='Regime change')
plt.title('Experts Predictions')
plt.xlabel('Time step')
plt.ylabel('Value')
plt.legend()

plt.tight_layout()
plt.show()

実験の結果

真の y からずれ始めるとエラーも増しますが、指数関数によって重みを瞬時に変化させることで直ぐに適応できていることがわかります。

このように「予想」をしなくても「変化に応じて臨機応変に対応」することもひとつの一手となり得るかもしれません。非常に簡単な例ですが参考になれば幸いです。

Discussion