🕌

Pythonでリザバーコンピューティングを体験してみる

に公開

はじめに

未踏ターゲットの募集が開始されたと聞き、サイトを見たところ、量子コンピュータに混ざって目に入ったのはリザバーコンピューティングという耳慣れないワードでした。目にしてしまえば試さずにはいられません。

この記事では、時系列予測問題を通して、リザバーコンピューティングの基本的な実装と、その過程で遭遇する典型的な問題をデバッグしていく様子を紹介します。

Step 1: シンプルなリザバーを実装する

まずは基本に忠実に、リザバーコンピューティングを実装してみましょう。データは、Prophetのチュートリアルでもおなじみの「ペイトン・マニングのWikipediaページビュー数」のデータセットを使います。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# ---------------------------------------------------------
# 1. データの準備
# ---------------------------------------------------------
url = 'https://raw.githubusercontent.com/facebook/prophet/main/examples/example_wp_log_peyton_manning.csv'
df = pd.read_csv(url)
data = df['y'].values # データは対数変換済み
data = data[:2000]   # 計算負荷と可視化のため2000日分に絞る

# 0-1に正規化
d_min, d_max = data.min(), data.max()
data_norm = (data - d_min) / (d_max - d_min)

# 訓練データとテストデータに分割
train_ratio = 0.8
train_len = int(len(data_norm) * train_ratio)
train_data = data_norm[:train_len]
test_data = data_norm[train_len:]

# ---------------------------------------------------------
# 2. リザバーの設計
# ---------------------------------------------------------
np.random.seed(42)
N_RES = 300            # リザバーのノード数
spectral_radius = 0.95 # リザバーのスペクトル半径
leak_rate = 0.3        # リーク率

W_in = (np.random.rand(N_RES, 1) - 0.5) * 2
W_res = (np.random.rand(N_RES, N_RES) - 0.5) * 2
rho_W = max(abs(np.linalg.eigvals(W_res)))
W_res *= spectral_radius / rho_W

# ---------------------------------------------------------
# 3. 学習フェーズ:リザバー状態の収集
# ---------------------------------------------------------
states = []
x = np.zeros((N_RES, 1))
train_inputs = train_data[:-1]
train_targets = train_data[1:]

for u in train_inputs:
    curr_state = np.tanh(np.dot(W_in, u) + np.dot(W_res, x))
    x = (1 - leak_rate) * x + leak_rate * curr_state
    states.append(x[:, 0])
states = np.array(states)

# ---------------------------------------------------------
# 4. 学習フェーズ:出力重みの計算
# ---------------------------------------------------------
reg = 1e-4 # リッジ回帰の正則化項
W_out = np.dot(np.linalg.inv(np.dot(states.T, states) + reg * np.eye(N_RES)), 
               np.dot(states.T, train_targets))

# ---------------------------------------------------------
# 5. 予測フェーズ:自律的な予測(フリーラン)
# ---------------------------------------------------------
test_predictions = []
current_input = train_data[-1]

for _ in range(len(test_data)):
    curr_state = np.tanh(np.dot(W_in, current_input) + np.dot(W_res, x))
    x = (1 - leak_rate) * x + leak_rate * curr_state
    
    pred = np.dot(x[:, 0], W_out)
    test_predictions.append(pred)
    
    # 【重要】予測値を次の入力にする(閉ループ)
    current_input = pred

# ---------------------------------------------------------
# 6. 可視化
# ---------------------------------------------------------
test_predictions = np.array(test_predictions) * (d_max - d_min) + d_min
test_targets_orig = test_data * (d_max - d_min) + d_min

plt.figure(figsize=(12, 6))
plt.plot(np.arange(len(data)), data, label="Actual PV (Log)", color='lightgray')
plt.plot(np.arange(train_len, len(data)), test_targets_orig, label="Test Target", color='blue')
plt.plot(np.arange(train_len, len(data)), test_predictions, label="Reservoir Prediction", color='red', linestyle='--')
plt.axvline(x=train_len, color='black', linestyle=':', label="Start Prediction")
plt.title(f"Peyton Manning PV Forecast (Reservoir Computing)\nN={N_RES}, Leak={leak_rate}")
plt.xlabel("Days")
plt.ylabel("Log Page Views")
plt.legend()
plt.show()

結果1:予測の暴走

なぜ暴走したのか?:マイクのハウリング現象

これは典型的な 「閉ループ(Closed Loop)の暴走」 です。予測フェーズで、私たちは「自分の予測値」を「次の入力値」として再利用しています。これは、現実世界で言えば 「マイクをスピーカーに近づけた状態(ハウリング)」 と同じ現象を引き起こすことがあります。

  1. 初期の安定: 予測開始直後(1600〜1750日頃)は、予測が安定しています。
  2. 事故の発生: 1750日頃、予測値が学習時に経験したことのない範囲(正規化した0〜1の範囲外)にわずかに逸脱します。
  3. パニックの連鎖: その「異常な値」が次の入力としてリザバーに投入されます。リザバーは未知の入力に混乱し(ニューロンが飽和し)、さらに極端な値を返します。
  4. システムの崩壊: この負のフィードバックが繰り返され、予測値は瞬く間に発散し、システム全体が機能不全に陥ります。

Step 2: 暴走を止める(出力のクリッピング)

最初の対策はシンプルです。予測値が「ありえない範囲」に飛び出さないように、強制的に[0, 1]の範囲に収めてしまいましょう。numpy.clip を使って、予測フェーズのコードを一行修正します。

# 予測フェーズ(Step 5)の一部を修正

# ... (状態更新は同じ)
    
# 2. 予測値の計算
pred = np.dot(x[:, 0], W_out)

# 【ここを修正!】
# 予測値が[0, 1]の範囲を超えるのを防ぐ安全装置
pred_clipped = np.clip(pred, 0.0, 1.0) 

test_predictions.append(pred) # 可視化のため、クリップ前の値も保持
    
# 3.【重要】クリップした値を次の入力にする
current_input = pred_clipped

結果2:止まらない振動

なぜ振動するのか?:「過敏すぎる」リザバー

発散は防げましたが、今度は予測が激しく上下に振動してしまいました。これはリザバーが 「過学習(Overfitting)」 を起こしている兆候です。

  • 現状: 学習時、リザバーは「クリーンな正解データ」に過剰に適合しようとします。そのため、ノイズのない理想的な入力に対しては非常に正確です。
  • 問題: しかし、自律的に予測を始めると、自身の予測には必ず微小な誤差が含まれます。この「少し汚れた入力」に対して、過学習したリザバーは「間違えた!」と過剰に反応し、逆方向に大きく修正してしまいます。この過剰修正の繰り返しが振動を生むのです。

今のリザバーは、舗装された綺麗な道路しか走ったことがないドライバーのようなもの。少し砂利道(予測誤差)に入っただけで、パニックになってハンドルを切りすぎているのです。

Step 3: 安定化させる(学習時のノイズ注入)

この振動を抑えるための最も効果的なテクニックの一つが 「学習時のノイズ注入」 です。学習データにあえて少量のノイズを加えることで、リザバーに「多少の誤差は許容範囲」だと学ばせ、頑健性(ロバスト性)を高めます。これは一種の正則化として機能します。

舗装路だけでなく、少し荒れた砂利道でも安定して走れるように訓練するイメージです。

# 学習フェーズ(Step 3)と予測フェーズ(Step 5)を最終形に修正

# ---------------------------------------------------------
# 3. 学習フェーズ:ノイズを注入して状態を収集
# ---------------------------------------------------------
states = []
x = np.zeros((N_RES, 1))
train_inputs = train_data[:-1]
train_targets = train_data[1:]

noise_level = 0.005 # ノイズの強さ(ハイパーパラメータ)
np.random.seed(42)

for u in train_inputs:
    # 【重要】入力に正規分布ノイズを加える
    noise = np.random.normal(0, noise_level)
    u_noisy = u + noise
    
    curr_state = np.tanh(np.dot(W_in, u_noisy) + np.dot(W_res, x))
    x = (1 - leak_rate) * x + leak_rate * curr_state
    states.append(x[:, 0])
states = np.array(states)

# (Step 4 の出力重み計算は同じ)

# ---------------------------------------------------------
# 5. 予測フェーズ:クリッピングを維持
# ---------------------------------------------------------
test_predictions = []
current_input = train_data[-1]

for _ in range(len(test_data)):
    curr_state = np.tanh(np.dot(W_in, current_input) + np.dot(W_res, x))
    x = (1 - leak_rate) * x + leak_rate * curr_state
    
    pred = np.dot(x[:, 0], W_out)
    pred_clipped = np.clip(pred, 0.0, 1.0) 
    test_predictions.append(pred)
    
    # 予測時はノイズを加えない
    current_input = pred_clipped

最終結果:安定した長期予測へ

なぜうまくいったのか?

ついに、リザバーは暴走も振動もせず、未来のデータ(テストデータ)の全体的なトレンドを滑らかに予測することができました。

  • ノイズ注入の効果: 学習時にノイズを経験したことで、リザバーは入力の細かな変動に過剰反応しなくなりました。これにより、自らの予測誤差をうまく吸収し、安定した軌道を描けるようになったのです。
  • クリッピングの効果: ノイズ注入だけでは防ぎきれない突発的な大きな誤差が発生した場合に備え、クリッピングは依然として重要な「安全装置」として機能します。

この結果は、リザバーコンピューティングが、単純な構造と学習方法にもかかわらず、時系列データが持つダイナミクスを捉え、長期的な予測タスクにおいて非常に強力なツールとなり得ることを示しています。

まとめ:リザバーコンピューティングの勘所

今回の試行錯誤から、リザバーコンピューティングをうまく機能させるためのいくつかの重要なポイントが見えてきました。

  1. フリーランは不安定になりがち: 学習時と異なり、自らの予測を頼りにするフリーラン(閉ループ)予測は、誤差が蓄積しやすく、本質的に不安定です。
  2. 頑健性(ロバスト性)が鍵: 予測を安定させるには、モデルが多少の誤差を許容できる頑健性を持つ必要があります。そのためのテクニックが「ノイズ注入」や「リッジ回帰」による正則化です。
  3. ハイパーパラメータ調整が重要: spectral_radius(過去の記憶の長さ)、leak_rate(変化への追従速度)、noise_level(頑健性の度合い)といったハイパーパラメータは、問題の性質に合わせて調整する必要があります。

リザバーコンピューティングは、内部の重みを固定し、出力層だけを線形回帰で学習するという驚くべきシンプルさで、複雑な時系列データを扱うことができます。本記事が、この興味深い計算モデルへの第一歩となれば幸いです。

Discussion