This notebook demonstrates the modular workflow for volatility modeling using the codebase in src/. We use both synthetic HAR data and real ETF data, and reproduce the key plots and model training steps from the original workflow.
Implements an approach inspired by Co-Training Realized Volatility Prediction with Neural Distributional Transformation (Du et al., ICAIF'23) in a modular, OOP codebase:
- Transform (embedder): learnable invertible 1D monotonic neural transform
f_θwith closed-form derivative and numerical inverse. - Predictor: HAR on the transformed series
z_t = f_θ(x_t)with daily/weekly/monthly components. - Joint training:
mle_flow: Gaussian residual likelihood onzplus change‑of‑variables term-log|f'(x)|.mse_x: MSE on original scale after inversionx̂_{t+1} = f_θ^{-1}(ẑ_{t+1}).
- Data: reads Parquet with OHLCV & Adj Close; computes realized volatility proxy (Garman–Klass) if
rvnot supplied.
# Imports for modular workflow
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import torch
from src.data import gen_HAR_ts
from src.data import load_parquet_data,load_csv_data
from src.data import realised_vol_for_ticker, realised_vol_for_tickers
from src.embedder.NormalisingFlow1D import NormalizingFlowEmbedder
from src.predictor.HARPredictor import NeuralHARMLP-
NeuralHARMLP (src/predictor/HARPredictor.py)
- A small MLP that takes HAR-style features computed from a rolling window (lookback) of past values:
- mean over last d1 steps, mean over last d2 steps, mean over last d3 steps.
- You control the window length with
lookbackand the aggregation spans withd1,d2,d3. - Trained with a standard regression loss (e.g., MSE) to predict the next-step target.
- A small MLP that takes HAR-style features computed from a rolling window (lookback) of past values:
-
gen_HAR_ts (src/data/gen_synth.py)
- Generates a synthetic series consistent with a HAR process and returns a design matrix X_synth (lag features) and y_target_synth (the next-step target).
- Use this to quickly prototype the training pipeline.
-
realised_vol_for_ticker (src/data/rv.py)
- Computes realized volatility features from raw OHLCV data for a selected ticker.
- Returns a 2D numpy array of [daily_vol, garman_klass_vol] that can be converted to a torch tensor.
# Generate synthetic HAR data
d1, d2, d3 = 2, 10, 30
b_0, b_1, b_2, b_3 = -0.5, 0.9, -0.7, -0.8
X_synth,y_target_synth = gen_HAR_ts(N=5000, d1=d1, d2=d2, d3=d3
, b_0=b_0, b1=b_1, b2=b_2, b3=b_3,
noise_std=0.05, seed=42)
X_synthtensor([[-0.2144, -0.2811, -0.2995],
[-0.2349, -0.2670, -0.3193],
[-0.2448, -0.2515, -0.3038],
...,
[-0.9247, -0.2766, -0.2643],
[-0.9504, -0.4165, -0.3019],
[-0.8997, -0.5410, -0.3384]])
We generate a synthetic series using a HAR process. The generator creates lagged features (X_synth) and a next-step target (y_target_synth). These are used to sanity-check our model training flow before moving to real data.
etf_dataset = load_csv_data('../data_download/etf_prices_1hour_adjusted.csv')We load raw OHLCV data from CSV into a pandas DataFrame. For realized volatility features, we later select a specific ticker and compute [daily_vol, garman_klass_vol].
vol_data = realised_vol_for_ticker(etf_dataset, 'VOO')
vol_data/home/yagyik/Dropbox/finance_trials/volatility_modelling/VolatilityFlowPredictor/src/data/rv.py:26: FutureWarning: DataFrameGroupBy.apply operated on the grouping columns. This behavior is deprecated, and in a future version of pandas the grouping columns will be excluded from the operation. Either pass `include_groups=False` to exclude the groupings or explicitly select the grouping columns after groupby to silence this warning.
gk_vol = df_ticker.groupby('date_only').apply(garman_klass_vol)
array([[0. , 0.00257693],
[0. , 0.00257693],
[0. , 0.00257693],
...,
[0.00334623, 0.00294795],
[0.00334623, 0.00294795],
[0.00334623, 0.00294795]], shape=(12724, 2))
We compute realized volatility features for the selected ticker (e.g., 'VOO') using the simplified function. The output vol_data contains two columns: [daily_vol, garman_klass_vol]. You can convert it to a torch tensor for modeling.
- No lookback or batch_size is involved here; this step prepares features from raw data.
import matplotlib.pyplot as plt
fig, axs = plt.subplots(2, 1, figsize=(12, 10), sharex=True)
# Plot X columns
for i in range(X_synth.shape[1]):
axs[0].plot(X_synth[:, i].detach().numpy(), label=f'X[:, {i}]')
axs[0].set_ylabel('Lagged Features',fontsize=20)
axs[0].set_title('Lagged Features (X)',fontsize=20)
axs[0].legend(fontsize=20)
# Plot y_target
axs[1].plot(y_target_synth.detach().numpy(), color='tab:orange', label='y_target')
axs[1].set_ylabel('Target',fontsize=20)
axs[1].set_title('Target (y_target)',fontsize=20)
axs[1].legend(fontsize=20)
plt.tight_layout()
plt.show()We plot the HAR lagged features X_synth and the corresponding target y_target_synth to understand the structure of the synthetic dataset before training.
- No train/test split here; this is exploratory visualization.
import torch
from torch.utils.data import DataLoader, TensorDataset
import torch.optim as optim
# X_synth: (T, L) or (T, F)? From gen_HAR_ts, it returns X_synth (T, 3) of HAR lags and y_target_synth (T,)
# For NeuralHARMLP, we want raw windows x of length L to compute HAR features internally.
# We'll use the first column of X_synth as a proxy time series, or build windows directly from y_target_synth.
# Build windows from y_target_synth as the base series
series = y_target_synth.detach().clone()
print(len(series))
lookback = 1000
X_list, y_list = [], []
for t in range(lookback, len(series) - 1):
X_list.append(series[t - lookback:t])
y_list.append(series[t + 1])
Xw = torch.stack(X_list) # (N, lookback)
yw = torch.stack(y_list).unsqueeze(-1) # (N, 1)
# Time-based split 80:20
N = Xw.shape[0]
print(N)
split = int(0.8 * N)
X_tr, X_te = Xw[:split], Xw[split:]
y_tr, y_te = yw[:split], yw[split:]
tr_loader = DataLoader(TensorDataset(X_tr, y_tr), batch_size=64, shuffle=False)
te_loader = DataLoader(TensorDataset(X_te, y_te), batch_size=64, shuffle=False)
model = NeuralHARMLP(input_dim=lookback, output_dim=1,
hidden_dim=64, d1=1, d2=5, d3=22,
optimizer_cls=optim.Adam,
loss_fn=torch.nn.MSELoss(), lr=1e-3)
model.configure_optimizer()
# Train
epochs = 200
for epoch in range(epochs):
for xb, yb in tr_loader:
xb = xb.squeeze(-1) if xb.dim() == 3 else xb # Ensure shape (batch, lookback)
yb = yb.squeeze(-1) if yb.dim() == 3 else yb # Ensure shape (batch, 1)
model.optimizer.zero_grad()
pred = model(xb)
loss = model.loss_fn(pred, yb)
loss.backward()
model.optimizer.step()
if (epoch + 1) % 10 == 0:
print(f"Epoch {epoch+1}/{epochs} - Train MSE: {loss.item():.6f}")
with torch.no_grad():
preds = []
gts = []
for xb, yb in te_loader:
xb = xb.squeeze(-1) if xb.dim() == 3 else xb
yb = yb.squeeze(-1) if yb.dim() == 3 else yb
p = model(xb)
preds.append(p)
gts.append(yb)
preds = torch.cat(preds, dim=0)
gts = torch.cat(gts, dim=0)
mse = torch.mean((preds - gts) ** 2).item()
rmse = mse ** 0.5
print(f"Test RMSE: {rmse:.6f}")4969
3968
Epoch 10/200 - Train MSE: 0.008031
Epoch 10/200 - Train MSE: 0.008031
Epoch 20/200 - Train MSE: 0.005620
Epoch 20/200 - Train MSE: 0.005620
Epoch 30/200 - Train MSE: 0.005666
Epoch 30/200 - Train MSE: 0.005666
Epoch 40/200 - Train MSE: 0.005373
Epoch 40/200 - Train MSE: 0.005373
Epoch 50/200 - Train MSE: 0.004018
Epoch 50/200 - Train MSE: 0.004018
Epoch 60/200 - Train MSE: 0.003535
Epoch 60/200 - Train MSE: 0.003535
Epoch 70/200 - Train MSE: 0.003862
Epoch 70/200 - Train MSE: 0.003862
Epoch 80/200 - Train MSE: 0.003491
Epoch 80/200 - Train MSE: 0.003491
Epoch 90/200 - Train MSE: 0.003777
Epoch 90/200 - Train MSE: 0.003777
Epoch 100/200 - Train MSE: 0.004041
Epoch 100/200 - Train MSE: 0.004041
Epoch 110/200 - Train MSE: 0.002831
Epoch 110/200 - Train MSE: 0.002831
Epoch 120/200 - Train MSE: 0.003675
Epoch 120/200 - Train MSE: 0.003675
Epoch 130/200 - Train MSE: 0.003496
Epoch 130/200 - Train MSE: 0.003496
Epoch 140/200 - Train MSE: 0.002738
Epoch 140/200 - Train MSE: 0.002738
Epoch 150/200 - Train MSE: 0.002860
Epoch 150/200 - Train MSE: 0.002860
Epoch 160/200 - Train MSE: 0.002775
Epoch 160/200 - Train MSE: 0.002775
Epoch 170/200 - Train MSE: 0.002634
Epoch 170/200 - Train MSE: 0.002634
Epoch 180/200 - Train MSE: 0.002952
Epoch 180/200 - Train MSE: 0.002952
Epoch 190/200 - Train MSE: 0.002731
Epoch 190/200 - Train MSE: 0.002731
Epoch 200/200 - Train MSE: 0.002575
Test RMSE: 0.092184
Epoch 200/200 - Train MSE: 0.002575
Test RMSE: 0.092184
We create rolling windows of length lookback from the synthetic target series and perform an 80:20 time-based split:
- lookback: number of past steps used to compute HAR features (means over the last d1, d2, d3 points). Larger values may capture longer memory but reduce the number of samples.
- batch_size: number of windows per optimization step; set via the DataLoader. Higher batch_size stabilizes gradients but uses more memory.
- Split policy: chronological; the first 80% of windows for training, last 20% for testing. This avoids leakage from shuffling.
We report Train MSE during epochs and final Test RMSE after training.
# Plot predictions vs targets
# with torch.no_grad():
# preds = model(X_te).squeeze().numpy()
# targets = y_te.squeeze().numpy()
plt.plot(gts, label='Targets')
plt.plot(preds, label='Predictions')
plt.xlabel('Time Step')
plt.ylabel('Value')
plt.title('Targets vs Predictions')
plt.legend()
plt.tight_layout()
plt.show()
# Scatter plot: predictions vs targets
plt.figure(figsize=(6, 6))
plt.scatter(gts, preds, alpha=0.5)
plt.xlabel('Targets')
plt.ylabel('Predictions')
plt.title('Scatter: Predictions vs Targets')
plt.tight_layout()
plt.show()# Prepare vol_tensor from vol_data
vol_tensor = torch.tensor(vol_data, dtype=torch.float32)
if vol_tensor.ndim == 1:
vol_tensor = vol_tensor.unsqueeze(1)
if vol_tensor.shape[1] == 1:
# Tile to 2D for flow if needed
vol_tensor = torch.cat([vol_tensor, vol_tensor], dim=1)
# Instantiate and configure NormalizingFlowEmbedder
flow = NormalizingFlowEmbedder(num_layers=8,
features=vol_tensor.shape[1],
hidden_features=32,
optimizer_cls=optim.Adam,
lr=1e-3)
flow.configure_optimizer()
# Train the flow model
inputs = vol_tensor
for i in range(1000):
x = inputs[torch.randperm(inputs.size(0))[:128]]
flow.optimizer.zero_grad()
flow.loss_fn = -flow.flow.log_prob(inputs=x).mean()
loss = -flow.flow.log_prob(inputs=x).mean()
loss.backward()
flow.optimizer.step()
if (i+1) % 200 == 0:
print(f"Epoch {i+1}: Loss={loss.item():.4f}")
# Transform and inverse transform
vol_transformed = flow.transform(vol_tensor)
inverse_transformed = flow.inverse_transform(vol_transformed)
fig, axs = plt.subplots(3, 2, figsize=(14, 14))
# Row 1: Original vol_tensor
axs[0, 0].hist(vol_tensor[:, 0].detach().numpy(), bins=50, color='tab:green', alpha=0.7)
axs[0, 0].set_title('Histogram of daily_vol (original)', fontsize=20)
axs[0, 0].set_xlabel('daily_vol', fontsize=20)
axs[0, 0].set_ylabel('Frequency', fontsize=20)
axs[0, 0].tick_params(axis='x', labelsize=20)
axs[0, 0].tick_params(axis='y', labelsize=20)
axs[0, 1].hist(vol_tensor[:, 1].detach().numpy(), bins=50, color='tab:red', alpha=0.7)
axs[0, 1].set_title('Histogram of garman_klass_vol (original)', fontsize=20)
axs[0, 1].set_xlabel('garman_klass_vol', fontsize=20)
axs[0, 1].set_ylabel('Frequency', fontsize=20)
axs[0, 1].tick_params(axis='x', labelsize=20)
axs[0, 1].tick_params(axis='y', labelsize=20)
# Row 2: Transformed volatilities
axs[1, 0].hist(vol_transformed[:, 0].detach().numpy(), bins=50, color='tab:purple', alpha=0.7)
axs[1, 0].set_title('Histogram of vol_transformed[:, 0]', fontsize=20)
axs[1, 0].set_xlabel('vol_transformed[:, 0]', fontsize=20)
axs[1, 0].set_ylabel('Frequency', fontsize=20)
axs[1, 0].tick_params(axis='x', labelsize=20)
axs[1, 0].tick_params(axis='y', labelsize=20)
axs[1, 1].hist(vol_transformed[:, 1].detach().numpy(), bins=50, color='tab:brown', alpha=0.7)
axs[1, 1].set_title('Histogram of vol_transformed[:, 1]', fontsize=20)
axs[1, 1].set_xlabel('vol_transformed[:, 1]', fontsize=20)
axs[1, 1].set_ylabel('Frequency', fontsize=20)
axs[1, 1].tick_params(axis='x', labelsize=20)
axs[1, 1].tick_params(axis='y', labelsize=20)
# Row 3: Inverse transformed
axs[2, 0].hist(inverse_transformed[:, 0].detach().numpy(), bins=50, color='tab:blue', alpha=0.7)
axs[2, 0].set_title('Histogram of inverse_transformed[:, 0]',fontsize=20)
axs[2, 0].set_xlabel('inverse_transformed[:, 0]', fontsize=20)
axs[2, 0].set_ylabel('Frequency', fontsize=20)
axs[2, 0].tick_params(axis='x', labelsize=20)
axs[2, 0].tick_params(axis='y', labelsize=20)
axs[2, 1].hist(inverse_transformed[:, 1].detach().numpy(), bins=50, color='tab:orange', alpha=0.7)
axs[2, 1].set_title('Histogram of inverse_transformed[:, 1]',fontsize=20)
axs[2, 1].set_xlabel('inverse_transformed[:, 1]', fontsize=20)
axs[2, 1].set_ylabel('Frequency', fontsize=20)
axs[2, 1].tick_params(axis='x', labelsize=20)
axs[2, 1].tick_params(axis='y', labelsize=20)
# If you add legends, use fontsize=20:
# axs[row, col].legend(fontsize=20)
plt.tight_layout()
plt.show()Epoch 200: Loss=-7.1608
Epoch 400: Loss=-3.7269
Epoch 400: Loss=-3.7269
Epoch 600: Loss=-4.5926
Epoch 600: Loss=-4.5926
Epoch 800: Loss=-6.7962
Epoch 800: Loss=-6.7962
Epoch 1000: Loss=-8.3836
Epoch 1000: Loss=-8.3836
import importlib
importlib.reload(importlib) # Ensure importlib itself is fresh
modules_to_reload = [
'src.data',
'src.data.utils',
'src.embedder.NormalisingFlow1D',
'src.predictor.HARPredictor',
'src.joint_models'
]
for mod in modules_to_reload:
try:
importlib.reload(importlib.import_module(mod))
except Exception as e:
print(f"Could not reload {mod}: {e}")
from src.data.utils import make_dataloaders
from src.joint_models.FlowHAR import FlowHARModel# Use vol_tensor from cell 8 and utility functions from previous cell
lookback = 44
old_mean = vol_tensor.mean(dim=0)
old_std = vol_tensor.std(dim=0)
print(old_mean, old_std)
normalised_vol_tensor = (vol_tensor - old_mean) / old_std
train_loader, val_loader = make_dataloaders(normalised_vol_tensor, lookback=lookback, split_ratio=0.8, batch_size=128)
# Instantiate model
flow_embedder = NormalizingFlowEmbedder(num_layers=8,
features=normalised_vol_tensor.shape[1],
hidden_features=32,
optimizer_cls=optim.Adam,
lr=1e-3)
har_predictor = NeuralHARMLP(input_dim=3 * normalised_vol_tensor.shape[1],
output_dim=normalised_vol_tensor.shape[1],
hidden_dim=64, d1=1, d2=5, d3=22,
optimizer_cls=optim.Adam,
loss_fn=torch.nn.MSELoss(), lr=1e-3)
model = FlowHARModel(flow_embedder, har_predictor,
loss_fn = torch.nn.MSELoss(),
lr=1e-3)
flow_embedder.configure_optimizer()
har_predictor.configure_optimizer()
model.configure_optimizer()
num_epochs = 50
train_losses, val_losses = [], []
best_val_loss = float('inf')
best_state_dict = None
for epoch in range(num_epochs):
model.train()
train_loss = 0.0
for xb, yb in train_loader:
model.optimizer.zero_grad()
preds = model(xb)
loss = torch.sqrt(model.loss_fn(preds, yb))
loss.backward()
model.optimizer.step()
train_loss += loss.item() * xb.size(0)
train_loss /= len(train_loader.dataset)
train_losses.append(train_loss)
model.eval()
val_loss = 0.0
with torch.no_grad():
for xb, yb in val_loader:
preds = model(xb)
loss = torch.sqrt(model.loss_fn(preds, yb))
val_loss += loss.item() * xb.size(0)
val_loss /= len(val_loader.dataset)
val_losses.append(val_loss)
print(f"Epoch {epoch+1}: Train RMSE={train_loss:.4f}, Val RMSE={val_loss:.4f}")
if val_loss < best_val_loss:
best_val_loss = val_loss
best_state_dict = {k: v.cpu() for k, v in model.state_dict().items()}
# Save learned weights
torch.save(best_state_dict, "flow_har_model_best.pth")
# Store predictions and targets from the test set for downstream evaluation
model.eval()
preds_list, targets_list = [], []
with torch.no_grad():
for xb, yb in val_loader:
preds = model(xb)
# Rescale predictions and targets to original scale
preds_rescaled = preds * old_std + old_mean
targets_rescaled = yb * old_std + old_mean
preds_list.append(preds_rescaled)
targets_list.append(targets_rescaled)
preds_test = torch.cat(preds_list, dim=0)
targets_test = torch.cat(targets_list, dim=0)tensor([0.0030, 0.0053]) tensor([0.0040, 0.0041])
Epoch 1: Train RMSE=2.0638, Val RMSE=0.7075
Epoch 1: Train RMSE=2.0638, Val RMSE=0.7075
Epoch 2: Train RMSE=0.6694, Val RMSE=0.5534
Epoch 2: Train RMSE=0.6694, Val RMSE=0.5534
Epoch 3: Train RMSE=0.5586, Val RMSE=0.4725
Epoch 3: Train RMSE=0.5586, Val RMSE=0.4725
Epoch 4: Train RMSE=0.5195, Val RMSE=0.4487
Epoch 4: Train RMSE=0.5195, Val RMSE=0.4487
Epoch 5: Train RMSE=0.5137, Val RMSE=0.4386
Epoch 5: Train RMSE=0.5137, Val RMSE=0.4386
Epoch 6: Train RMSE=0.5051, Val RMSE=0.4373
Epoch 6: Train RMSE=0.5051, Val RMSE=0.4373
Epoch 7: Train RMSE=0.5011, Val RMSE=0.4343
Epoch 7: Train RMSE=0.5011, Val RMSE=0.4343
Epoch 8: Train RMSE=0.4912, Val RMSE=0.4348
Epoch 8: Train RMSE=0.4912, Val RMSE=0.4348
Epoch 9: Train RMSE=0.4892, Val RMSE=0.4344
Epoch 9: Train RMSE=0.4892, Val RMSE=0.4344
Epoch 10: Train RMSE=0.4918, Val RMSE=0.4538
Epoch 10: Train RMSE=0.4918, Val RMSE=0.4538
Epoch 11: Train RMSE=0.4855, Val RMSE=0.4291
Epoch 11: Train RMSE=0.4855, Val RMSE=0.4291
Epoch 12: Train RMSE=0.4977, Val RMSE=0.4436
Epoch 12: Train RMSE=0.4977, Val RMSE=0.4436
Epoch 13: Train RMSE=0.4822, Val RMSE=0.4301
Epoch 13: Train RMSE=0.4822, Val RMSE=0.4301
Epoch 14: Train RMSE=0.4816, Val RMSE=0.4298
Epoch 14: Train RMSE=0.4816, Val RMSE=0.4298
Epoch 15: Train RMSE=0.4732, Val RMSE=0.4462
Epoch 15: Train RMSE=0.4732, Val RMSE=0.4462
Epoch 16: Train RMSE=0.4834, Val RMSE=0.4316
Epoch 16: Train RMSE=0.4834, Val RMSE=0.4316
Epoch 17: Train RMSE=0.4755, Val RMSE=0.4298
Epoch 17: Train RMSE=0.4755, Val RMSE=0.4298
Epoch 18: Train RMSE=0.4727, Val RMSE=0.4261
Epoch 18: Train RMSE=0.4727, Val RMSE=0.4261
Epoch 19: Train RMSE=0.4730, Val RMSE=0.4254
Epoch 19: Train RMSE=0.4730, Val RMSE=0.4254
Epoch 20: Train RMSE=0.4751, Val RMSE=0.4175
Epoch 20: Train RMSE=0.4751, Val RMSE=0.4175
Epoch 21: Train RMSE=0.4764, Val RMSE=0.4158
Epoch 21: Train RMSE=0.4764, Val RMSE=0.4158
Epoch 22: Train RMSE=0.4656, Val RMSE=0.4181
Epoch 22: Train RMSE=0.4656, Val RMSE=0.4181
Epoch 23: Train RMSE=0.4697, Val RMSE=0.4270
Epoch 23: Train RMSE=0.4697, Val RMSE=0.4270
Epoch 24: Train RMSE=0.4707, Val RMSE=0.4173
Epoch 24: Train RMSE=0.4707, Val RMSE=0.4173
Epoch 25: Train RMSE=0.4681, Val RMSE=0.4224
Epoch 25: Train RMSE=0.4681, Val RMSE=0.4224
Epoch 26: Train RMSE=0.4753, Val RMSE=0.4380
Epoch 26: Train RMSE=0.4753, Val RMSE=0.4380
Epoch 27: Train RMSE=0.4643, Val RMSE=0.4150
Epoch 27: Train RMSE=0.4643, Val RMSE=0.4150
Epoch 28: Train RMSE=0.4666, Val RMSE=0.4238
Epoch 28: Train RMSE=0.4666, Val RMSE=0.4238
Epoch 29: Train RMSE=0.4619, Val RMSE=0.4219
Epoch 29: Train RMSE=0.4619, Val RMSE=0.4219
Epoch 30: Train RMSE=0.4577, Val RMSE=0.4119
Epoch 30: Train RMSE=0.4577, Val RMSE=0.4119
Epoch 31: Train RMSE=0.4555, Val RMSE=0.4137
Epoch 31: Train RMSE=0.4555, Val RMSE=0.4137
Epoch 32: Train RMSE=0.4545, Val RMSE=0.4155
Epoch 32: Train RMSE=0.4545, Val RMSE=0.4155
Epoch 33: Train RMSE=0.4626, Val RMSE=0.4487
Epoch 33: Train RMSE=0.4626, Val RMSE=0.4487
Epoch 34: Train RMSE=0.4587, Val RMSE=0.4189
Epoch 34: Train RMSE=0.4587, Val RMSE=0.4189
Epoch 35: Train RMSE=0.4566, Val RMSE=0.4269
Epoch 35: Train RMSE=0.4566, Val RMSE=0.4269
Epoch 36: Train RMSE=0.4544, Val RMSE=0.4204
Epoch 36: Train RMSE=0.4544, Val RMSE=0.4204
Epoch 37: Train RMSE=0.4500, Val RMSE=0.4174
Epoch 37: Train RMSE=0.4500, Val RMSE=0.4174
Epoch 38: Train RMSE=0.4527, Val RMSE=0.4140
Epoch 38: Train RMSE=0.4527, Val RMSE=0.4140
Epoch 39: Train RMSE=0.4547, Val RMSE=0.4100
Epoch 39: Train RMSE=0.4547, Val RMSE=0.4100
Epoch 40: Train RMSE=0.4593, Val RMSE=0.4418
Epoch 40: Train RMSE=0.4593, Val RMSE=0.4418
Epoch 41: Train RMSE=0.4632, Val RMSE=0.4087
Epoch 41: Train RMSE=0.4632, Val RMSE=0.4087
Epoch 42: Train RMSE=0.4533, Val RMSE=0.4139
Epoch 42: Train RMSE=0.4533, Val RMSE=0.4139
Epoch 43: Train RMSE=0.4569, Val RMSE=0.4186
Epoch 43: Train RMSE=0.4569, Val RMSE=0.4186
Epoch 44: Train RMSE=0.4497, Val RMSE=0.4256
Epoch 44: Train RMSE=0.4497, Val RMSE=0.4256
Epoch 45: Train RMSE=0.4537, Val RMSE=0.4246
Epoch 45: Train RMSE=0.4537, Val RMSE=0.4246
Epoch 46: Train RMSE=0.4489, Val RMSE=0.4049
Epoch 46: Train RMSE=0.4489, Val RMSE=0.4049
Epoch 47: Train RMSE=0.4426, Val RMSE=0.4034
Epoch 47: Train RMSE=0.4426, Val RMSE=0.4034
Epoch 48: Train RMSE=0.4380, Val RMSE=0.4139
Epoch 48: Train RMSE=0.4380, Val RMSE=0.4139
Epoch 49: Train RMSE=0.4521, Val RMSE=0.4021
Epoch 49: Train RMSE=0.4521, Val RMSE=0.4021
Epoch 50: Train RMSE=0.4417, Val RMSE=0.4077
Epoch 50: Train RMSE=0.4417, Val RMSE=0.4077
fig, axs = plt.subplots(2, 2, figsize=(14, 10))
# Line plots for each dimension
for i in range(preds_test.shape[1]):
axs[0, i].plot(targets_test[:, i].detach().numpy(), label='Target', color='tab:blue')
axs[0, i].plot(preds_test[:, i].detach().numpy(), label='Prediction', color='tab:orange')
axs[0, i].set_title(f'Lineplot: Dimension {i+1}', fontsize=16)
axs[0, i].set_xlabel('Time Step', fontsize=14)
axs[0, i].set_ylabel('Value', fontsize=14)
axs[0, i].legend(fontsize=12)
# Scatter plots for each dimension
for i in range(preds_test.shape[1]):
axs[1, i].scatter(targets_test[:, i].detach().numpy(), preds_test[:, i].detach().numpy(), alpha=0.5)
axs[1, i].set_title(f'Scatter: Pred vs Target (Dim {i+1})', fontsize=16)
axs[1, i].set_xlabel('Target', fontsize=14)
axs[1, i].set_ylabel('Prediction', fontsize=14)
plt.tight_layout()
plt.show()We visualize distributions of transformed features, predictions vs targets, and residuals. These help assess calibration and model fit quality.
- No train/test split changes here; evaluation pertains to the synthetic split defined earlier.




