Neural ODEs: Does Continuous Time Beat a Delta-Time Feature?
ニューラル常微分方程式(Chen ら、NeurIPS 2018)は、隠れ状態の微分をニューラルネットワークでパラメータ化し、ODE ソルバーに出力を計算させます。モデルは連続深度になり、市場にとってさらに有用な連続時間モデルになります。隠れ状態は任意の で評価できます。
この性質は、ランダムな間隔で到着する金融データに明らかな利点がありそうです。しかし、その主張は見た目より弱いものです。通常の GRU に、前回の観測からの経過時間を表す という入力特徴量を追加するだけでも、経過時間は「分かり」ます。本当の問いは、連続時間モデルが不規則データを取り込めるかではなく、観測間で学習した連続ダイナミクスが、差分時間特徴量では得られない情報を抽出できるかです。しかも推論コストは 10 倍から 100 倍になります。
この記事ではその実験を組み立て、実行に必要なツールチェーンを示し、正当化に必要な比較結果を説明します。バー構築の論点、つまり取引タイミング自体が信号を持つという主張は、時間バーを超えてで 17 種類のバーを用いた実測により確立されています。ここで重要な帰結は、ソルバーが任意の で隠れ状態を評価するため、補間やパディングが不要だという点です。
検証する主張

最後のものほど厳しい、3つの入れ子状の仮説です。
- H1 — 生の不規則な BTC 取引ストリームで学習した ODE-RNN が、時間情報を与えない同じストリーム上の GRU を上回る。ほぼ確実ですが、ほぼ価値がありません。ODE-RNN にベースラインにない情報があるだけだからです。
- H2 — GRU に入力特徴量として を与えた後も、ODE-RNN が同じ GRU を上回る。これが誠実な検証です。Neural ODE 文献から通常読み取られる主張ですが、金融データではほとんど実証されていません。
- H3 — H2 の優位性がレイテンシ予算を満たす。つまり、自適応
dopri5ではなく、ライブ推論に十分速い固定ステップソルバーで ODE-RNN をデプロイしても成立する。
実験プロトコル
両方の方式で同じ tick ストリーム、同じターゲット、同じ調整予算を使います。1m バーへのリサンプリングは行いません。リサンプリングは検証対象の正確な構造を壊します。
| 設定 | |
|---|---|
| データ | BTC の約定記録。到着間隔は可変で、リサンプリングしない |
| 方式 | ODE-RNN;GRU + ;時間特徴量なしの GRU |
| ターゲット | すべての方式で同じ。フィット時に指定 |
| 指標 | RMSE、ホールドアウト対数尤度、エポック当たりの実時間、ステップ当たり推論レイテンシ |
| ソルバー比較 | dopri5(rtol 1e-5)対固定ステップ Euler。学習精度を一致させる |
| 分割 | Walk-forward、サンプル外のみ |
| 方式 | RMSE | 対数尤度 | 秒/エポック | ステップ当たり推論レイテンシ |
|---|---|---|---|---|
| GRU(時間特徴量なし) | — | — | — | — |
| GRU + log(dt) | — | — | — | — |
| ODE-RNN(dopri5) | — | — | — | — |
| ODE-RNN(固定ステップ Euler) | — | — | — | — |
H2 の負の結果は十分に公開でき、より価値の高い結果かもしれません。誠実な負の結果と多重検定下のディフレート Sharpeで適用されるのと同じ基準です。
二次実験:CNF 対 Student-t
ODE-RNN の比較が高コストすぎる場合、より安価な実証上の基準は分布モデルです。実際の BTC 日次リターンに連続正規化フローをフィットし、その密度を Student-t のフィットおよび経験的な裾と比較して、1% と 5% 水準の裾分位点誤差を報告します。これはGARCH ボラティリティ予測と非対称 GARCHで測定された分布モデリングに直接つながります。
この記事の残りでは、上記を実行するために必要な背景とツールチェーンを説明します。
背景:ResNet から連続ダイナミクスへ

残差ネットワークは を計算します。ステップ幅を小さくして層数を増やすと、連続極限に近づきます。
独立パラメータを持つ 個の離散層の代わりに、単一のネットワーク が瞬間的な変化率を指定します。時刻 の出力は初期値問題を解きます。
ブラックボックスソルバー(Euler、Runge-Kutta、Dormand-Prince)が積分を数値計算し、局所誤差推定からステップ幅を適応的に選びます。
随伴法
全ソルバーステップを通して逆伝播すると中間状態をすべて保存するため、メモリはステップ数に比例します。Chen らは メモリで後向き ODE を解きます。随伴状態 を定義すると、これは
後向きパスに沿ってパラメータ勾配を蓄積します。
後向きパスでは、、、 を同時に計算する拡張システムを解き、ソルバーを から へ逆向きに実行します。
トレードオフは、後向きに を再構成すると数値誤差が蓄積することです。剛性やカオス的なダイナミクスでは特に深刻です。チェックポイントは折衷案で、いくつかの中間時刻に を保存して間を再計算します。価格過程は連続半マルチンゲールで比較的滑らかなため、随伴法は通常機能しますが、マイクロストラクチャイベント周辺の剛性により適応ソルバーやハイブリッドが必要になることがあります。
ODE-RNN:観測間の連続隠れ状態

ODE-RNN は主実験で使うアーキテクチャです。観測間では、隠れ状態が ODE に従って進化します。
観測 が に到着すると、離散更新が発生します。
上付き文字は観測の直前と直後の状態を表します。観測間では学習された連続ダイナミクスが働き、観測時には新しい情報が入ります。
H2 消融が検証する仕組みは次のとおりです。長い空白があると、隠れ状態は の下で長く進化し、ベースラインへ減衰するか発散します。その進化の形はスカラーとして与えられるのではなく学習されます。この表現力が実際の取引データで元を取れるかどうかこそ、まだ測定されていません。
tick 以外の自然な適用先には、資産ごとに観測スケジュールが異なり、相関資産の潜在状態が一つだけ観測されている間も進化するマルチアセットポートフォリオや、ニュース、決算、マクロ発表のように不規則な時刻に到着するイベント駆動シグナルがあります。
Neural SDE:確率成分の追加

Neural ODE は決定論的で、価格経路のノイズを表現できません。幾何ブラウン運動、リスク中立ドリフト、一定ボラティリティ仮定がボラティリティスマイルに対して破綻する理由を含む古典的な扱いは、Black-Scholes オプション価格付けで説明しています。Neural SDE はパラメトリックな形を学習された拡散項に置き換えます。
はドリフト、 は拡散、 は Wiener 過程で、 と はどちらもニューラルネットワークです。
アーキテクチャ:ドリフトネットと拡散ネット
- ドリフトネット :トレンド、平均回帰、モメンタムを含む期待経路。予測誤差を最小化するよう学習します。
- 拡散ネット :状態の関数として学習されるノイズの大きさ。高ボラティリティ局面では大きく、穏やかな局面では小さくなります。
この分割は古典的なクオンツ金融に対応します。ドリフトはリスク中立(または P 測度)のダイナミクスで、拡散はボラティリティサーフェスです。
Neural SDE の学習
確率積分 は古典的な意味では微分可能ではありません。方法は3つあります。
- パスワイズ勾配:再パラメータ化トリックでブラウン経路をサンプルし、ノイズを固定入力として扱ってソルバーを通じて微分します。
- スコアマッチング: を推定し、デノイジング目的で学習します。これは暗号資産予測の拡散モデルで独立した生成モデルとして使われる仕組みと同じで、ここでは SDE の学習手段に位置付けます。
- 有限次元分布マッチング:完全な経路測度ではなく、観測時刻の周辺分布を合わせます。
実用上のデフォルトはパスワイズ法です。torchsde は Euler-Maruyama、Milstein、確率的 Runge-Kutta を自動微分対応で実装します。
ボラティリティサーフェスの学習
古典モデル(Heston、SABR)は拡散係数にパラメトリックな形を課します。Neural SDE は を任意の関数として学習します。
これは拡散過程の万能近似器です。十分な容量があれば任意の Ito 過程を任意の精度で近似できます。
欠測・非同期データの潜在 ODE

潜在 ODE(Rubanova、Chen、Duvenaud、2019)は Neural ODE と VAE を組み合わせます。金融系列は滑らかな基礎過程のノイズ観測であり、その過程を低次元の潜在空間で学習します。
- 認識ネットワーク:観測を逆向きに走査する ODE-RNN で を生成します。
- 潜在ダイナミクス:。
- デコーダ:。任意の要求時刻で評価できます。
損失は標準的な ELBO です。
ポートフォリオ状態推定:資産間の疎な非同期観測から、共同ダイナミクスを捉える連続潜在状態を推定します。本質的にはカルマンフィルタの非線形な学習版です。
欠測データ補完:取引停止や週末の空白に対して滑らかに補間された潜在軌跡を得て、デコーダは VAE 事後分布の不確実性を伴うもっともらしい経路を空白内に生成します。
マルチ周波数融合:日次終値、日中 VWAP、tick データを共通の時間グリッドなしで一つのモデルに入れます。
リターン分布の連続正規化フロー

CNF は Neural ODE を使い、単純な基底分布を複雑なターゲットへ変換します。変換は で、対数密度は瞬間的な変数変換に従います。
状態と対数密度を から前向きに積分し、 と を得ます。スケーラビリティのため、ヤコビアンのトレースは Hutchinson の確率的推定量で推定します。
暗号資産のリターンは尖度が高く負に歪んでおり、実データではGARCH ボラティリティ予測と非対称 GARCH とレバレッジ効果で測定されています。CNF はその形を仮定するのではなく学習します。状態変数でフローを条件付ければ、形状を局面に応じて変えられます。密度は近似ではなく厳密なので、モンテカルロとブートストラップのバックテストで計算する裾指標、HRP/CVaR ポートフォリオパイプラインの CVaR 構築に渡せます。共同の裾構造は共同リスクのコピュラモデルで別途扱います。
条件付き密度推定
同じ問題への3つの発表済みアプローチを直接対比すると分かりやすくなります。TFTは分位点出力層から固定された分位点集合を返します。コンフォーマル予測はカバレッジ保証付きの校正区間を返します。条件付き CNF は厳密で微分可能な密度を返します。
微分可能性が決定的な違いです。密度を下流目的の中に置いて逆伝播できますが、固定分位点もコンフォーマル区間も対応しません。同じターゲットで、TFT の分位点に対して厳密な密度がコストに見合うかは、ここでは未検証です。
離散的な代替手法との比較

| 特性 | LSTM/GRU | Transformer | Neural ODE | Neural SDE |
|---|---|---|---|---|
| 不規則時間の扱い | 不十分(パディングが必要) | 位置エンコーディング | ネイティブ | ネイティブ |
| メモリ(学習) | 随伴法 | 随伴法 | ||
| 不確実性の定量化 | なし(決定論的) | なし(決定論的) | アンサンブル経由 | ネイティブ |
| 観測間の補間 | なし | なし | あり | あり |
| 連続時間密度 | なし | なし | CNF 経由 | 経路測度経由 |
| 計算コスト | 低 | 中 | 可変(ソルバーによる) | 高(SDE ソルバー) |
この表の LSTM 対アテンション部分は、ベンチマークとともにTFT 記事で詳しく論じています。「TFT vs LSTM vs Vanilla Transformer」と「When LSTM Still Wins」の節を参照してください。ここで新しいのは右側の2列で、H2/H3 の問いを決める行は最後の2行です。
torchdiffeq による Python 実装

torchdiffeq は随伴バックプロパゲーション付きの ODE ソルバーを提供します。
価格ダイナミクスの基本 Neural ODE
import torch
import torch.nn as nn
from torchdiffeq import odeint_adjoint as odeint
class PriceDynamics(nn.Module):
"""Neural network defining dh/dt = f(h, t)."""
def __init__(self, hidden_dim: int = 64):
super().__init__()
self.net = nn.Sequential(
nn.Linear(hidden_dim, 128),
nn.Tanh(),
nn.Linear(128, 128),
nn.Tanh(),
nn.Linear(128, hidden_dim),
)
def forward(self, t, h):
return self.net(h)
class NeuralODEPredictor(nn.Module):
"""
Encode observed features -> latent state,
evolve via Neural ODE,
decode to price prediction.
"""
def __init__(self, input_dim: int, hidden_dim: int = 64):
super().__init__()
self.encoder = nn.Linear(input_dim, hidden_dim)
self.dynamics = PriceDynamics(hidden_dim)
self.decoder = nn.Linear(hidden_dim, 1)
def forward(self, x0, eval_times):
"""
x0: (batch, input_dim) features at t=0
eval_times: (T,) times at which to evaluate the ODE
Returns: (T, batch, 1) predictions
"""
h0 = self.encoder(x0) # (batch, hidden_dim)
h_traj = odeint(self.dynamics, h0, eval_times,
method='dopri5', rtol=1e-5, atol=1e-7)
return self.decoder(h_traj)
不規則 tick データ向け ODE-RNN
これは実験側です。上回る必要があるベースラインは、同じストリーム上で nn.GRUCell を使い、log(t_next - t_prev) を x に連結したものです。
class ODERNNCell(nn.Module):
"""Single step: ODE-evolve, then RNN-update."""
def __init__(self, input_dim: int, hidden_dim: int = 64):
super().__init__()
self.dynamics = PriceDynamics(hidden_dim)
self.gru_cell = nn.GRUCell(input_dim, hidden_dim)
def forward(self, h, x, t_prev, t_next):
times = torch.tensor([t_prev, t_next], dtype=torch.float32)
h_evolved = odeint(self.dynamics, h, times,
method='dopri5')[-1] # state at t_next
h_updated = self.gru_cell(x, h_evolved)
return h_updated
class ODERNN(nn.Module):
"""Process irregularly-sampled sequence."""
def __init__(self, input_dim: int, hidden_dim: int = 64):
super().__init__()
self.cell = ODERNNCell(input_dim, hidden_dim)
self.decoder = nn.Linear(hidden_dim, 1)
self.hidden_dim = hidden_dim
def forward(self, observations, times):
"""
observations: list of (batch, input_dim) tensors
times: list of floats, observation timestamps
"""
batch_size = observations[0].shape[0]
h = torch.zeros(batch_size, self.hidden_dim)
outputs = []
for i in range(len(observations)):
t_prev = 0.0 if i == 0 else times[i - 1]
h = self.cell(h, observations[i], t_prev, times[i])
outputs.append(self.decoder(h))
return torch.stack(outputs) # (seq_len, batch, 1)
学習ループ
def train_neural_ode(model, train_loader, epochs=100, lr=1e-3):
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=epochs
)
for epoch in range(epochs):
epoch_loss = 0.0
for batch in train_loader:
features, times, targets = batch
optimizer.zero_grad()
predictions = model(features, times)
loss = torch.nn.functional.mse_loss(predictions, targets)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
epoch_loss += loss.item()
scheduler.step()
if (epoch + 1) % 10 == 0:
avg_loss = epoch_loss / len(train_loader)
print(f"Epoch {epoch+1}/{epochs}, Loss: {avg_loss:.6f}")
リターン分布向け連続正規化フロー
from torchdiffeq import odeint
class CNFDynamics(nn.Module):
"""Dynamics for continuous normalizing flow."""
def __init__(self, dim: int = 1, hidden_dim: int = 64):
super().__init__()
self.net = nn.Sequential(
nn.Linear(dim + 1, hidden_dim), # +1 for time
nn.Tanh(),
nn.Linear(hidden_dim, hidden_dim),
nn.Tanh(),
nn.Linear(hidden_dim, dim),
)
self.dim = dim
def forward(self, t, state):
z = state[..., :self.dim]
t_expand = t.expand(z.shape[0], 1)
zt = torch.cat([z, t_expand], dim=-1)
dz = self.net(zt)
e = torch.randn_like(z)
e_dz = torch.autograd.grad(
dz, z, e, create_graph=True
)[0]
trace_jac = (e_dz * e).sum(dim=-1, keepdim=True)
return torch.cat([dz, -trace_jac], dim=-1)
class ReturnDistributionCNF(nn.Module):
"""Model return distributions with continuous normalizing flows."""
def __init__(self, dim: int = 1):
super().__init__()
self.dynamics = CNFDynamics(dim)
self.dim = dim
def log_prob(self, x):
"""Compute log probability of observed returns."""
log_p0 = torch.zeros(x.shape[0], 1)
state0 = torch.cat([x, log_p0], dim=-1)
state0.requires_grad_(True)
times = torch.tensor([1.0, 0.0]) # backward
state_T = odeint(self.dynamics, state0, times,
method='dopri5')[-1]
z_T = state_T[..., :self.dim]
delta_log_p = state_T[..., self.dim:]
log_p_base = -0.5 * (z_T ** 2 + torch.log(
torch.tensor(2 * torch.pi)
)).sum(dim=-1, keepdim=True)
return log_p_base + delta_log_p
def sample(self, n_samples: int):
"""Generate samples from learned distribution."""
z0 = torch.randn(n_samples, self.dim)
log_p0 = torch.zeros(n_samples, 1)
state0 = torch.cat([z0, log_p0], dim=-1)
times = torch.tensor([0.0, 1.0]) # forward
state_T = odeint(self.dynamics, state0, times,
method='dopri5')[-1]
return state_T[..., :self.dim]
実践上の注意

以下は、上の実験を実行すると問題になりやすい点です。
ソルバーの選択と速度
dopri5 は精度を保証しますが計算コストが可変です。そのため H3 は H2 とは別の仮説です。適応ソルバーの下でしか存在しない優位性は、ライブのレイテンシ予算では維持できないかもしれません。固定ステップソルバー(Euler、RK4)は精度と引き換えに予測可能なレイテンシを提供します。実務的な折衷案は、dopri5 で学習し、代表データで適応ソルバーの出力に合うよう校正した固定ステップソルバーでデプロイすることです。そして差を測定し、小さいと仮定しません。
odeint(f, h0, t, method='dopri5', rtol=1e-6, atol=1e-8)
odeint(f, h0, t, method='euler', options={'step_size': 0.1})
不連続なジャンプ付近の剛性問題には、method='implicit_adams' または method='scipy_solver' を使います。
数値安定性
が大きな値を出力すると状態は発散します。対策は次のとおりです。
- の層にスペクトル正規化を適用し、Lipschitz 定数を制御する。
- 学習中に勾配をクリップする(上の学習ループを参照)。
- 時間を正規化し、タイムスタンプを にスケールする。
- ダイナミクスのノルムを正則化し、 を損失に加える。
積分区間
数か月にまたがるデータでは、 から 分まで積分しないでください。
t_normalized = (timestamps - timestamps[0]) / (timestamps[-1] - timestamps[0])
これによりソルバーを数値的に扱いやすい範囲に保ち、学習されたダイナミクスをスケール不変にできます。正規化されていない ODE-RNN が純粋に数値的な理由で GRU ベースラインに負けるなら、それは発見ではなく測定アーティファクトです。
複数の時間スケールへの対応
市場にはマイクロ秒、秒、分、日次のダイナミクスが同時に存在し、単一 Neural ODE ではすべてを保持しにくい場合があります。異なる時間スケールで複数の ODE ブロックを積む、高速・低速の両方に対応できるよう隠れ次元を拡張する、周波数帯ごとに Neural ODE を実行して出力を融合する、といった選択肢があります。(これはモデル容量の問題であり、適応解像度の深掘りにおけるバックテスト忠実度の問題とは異なります。)
オープンな研究方向

Neural Jump SDE:フラッシュクラッシュや決算サプライズのような突然の市場の断絶に対する学習済みジャンプ成分を追加します。
ここで は補償ポアソン確率測度、 は学習されたジャンプ核です。
Neural Controlled Differential Equations(Neural CDE):Wiener 過程を一般の駆動信号に置き換え、観測データストリームにダイナミクスを駆動させます。取引と気配値のストリームが潜在市場状態を駆動する注文フローに自然な方法です。
微分可能な市場シミュレーション:Neural SDE を微分可能シミュレータ内の生成モデルとして使い、随伴法を通じて逆伝播しながら戦略をエンドツーエンドで学習します。戦略と市場モデルが共進化します。
物理情報付き Neural ODE:PINN は微分方程式の残差を損失に埋め込みます。この技術自体はNavier-Stokes 論文で紹介されています。金融への応用では、無裁定、プットコールパリティ、マルチンゲール条件をペナルティ項または学習ダイナミクスへのハード制約として課します。
結論

連続時間という枠組みは構造的に正しいものです。市場は連続過程であり、離散的で不規則な時刻に観測されるため、モデルはそれを尊重すべきです。ツールチェーンは成熟しています。torchdiffeq、torchsde、その他に使う通常の PyTorch があり、既知のコストはソルバー速度と長い積分区間での数値安定性です。
まだ確立していないのは、これらのどの部分を本番スタックに入れるべきかです。構造的に正しいことは予測優位の証拠ではなく、 特徴量に対して連続ダイナミクスが持つとされる具体的な優位性は、実際の取引データで測定されていません。H2 表が埋まるまでは、折りたたみ線より下を、動く実装付きの明確な仮説として扱い、結果とは見なさないでください。
参考文献
- Chen, R.T.Q., Rubanova, Y., Bettencourt, J., Duvenaud, D. (2018). Neural Ordinary Differential Equations。NeurIPS 2018。arXiv:1806.07366
- Rubanova, Y., Chen, R.T.Q., Duvenaud, D. (2019). 不規則サンプリング時系列の Latent ODE。NeurIPS 2019。arXiv:1907.03907
- Jia, J., Benson, A.R. (2019). Neural Jump Stochastic Differential Equations。NeurIPS 2019。
- Kidger, P., Morrill, J., Foster, J., Lyons, T. (2020). 不規則時系列の Neural Controlled Differential Equations。NeurIPS 2020。
- Hasan, A., Pereira, J.M., Farsiu, S., Carin, L. (2021). 金融データ予測への応用を伴うニューラルネットワーク確率微分方程式モデル。arXiv:2111.13164
- torchdiffeq:github.com/rtqichen/torchdiffeq
- UvA Deep Learning Tutorials — Neural ODE:uvadlc-notebooks.readthedocs.io
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.