feat(ml): add KAN architecture + TLOB/KAN trainable/hyperopt adapters
Phase 2-3 of ensemble expansion: - KAN (Kolmogorov-Arnold Network): B-spline basis, layer, network, trainable adapter - TLOB UnifiedTrainable adapter with 3D input support (batch, seq, features) - Hyperopt adapters for both KAN and TLOB (ParameterSpace + metrics) - ModelType::KAN variant registered in common, coordinator, lib.rs - 44 new tests, all passing, zero warnings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
217
ml/src/hyperopt/adapters/kan.rs
Normal file
217
ml/src/hyperopt/adapters/kan.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
//! KAN (Kolmogorov-Arnold Network) Hyperparameter Optimization Adapter
|
||||
//!
|
||||
//! Provides `KANParams` (implementing `ParameterSpace`) and `KANMetrics`
|
||||
//! for hyperparameter optimization of the KAN model via the unified
|
||||
//! optimization framework.
|
||||
|
||||
use crate::hyperopt::traits::ParameterSpace;
|
||||
use crate::MLError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Hyperparameters for KAN hyperopt tuning.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct KANParams {
|
||||
/// Learning rate for AdamW optimizer (log-scale)
|
||||
pub learning_rate: f64,
|
||||
/// Number of B-spline grid points per activation
|
||||
pub grid_size: usize,
|
||||
/// B-spline order (degree + 1)
|
||||
pub spline_order: usize,
|
||||
/// Hidden layer width (used for all hidden layers)
|
||||
pub hidden_width: usize,
|
||||
/// Number of hidden layers (total layers = num_layers + 1 for output)
|
||||
pub num_layers: usize,
|
||||
/// L2 weight decay (log-scale)
|
||||
pub weight_decay: f64,
|
||||
/// Maximum gradient norm for clipping (log-scale)
|
||||
pub grad_clip: f64,
|
||||
/// Training batch size
|
||||
pub batch_size: usize,
|
||||
}
|
||||
|
||||
impl Default for KANParams {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
learning_rate: 1e-3,
|
||||
grid_size: 5,
|
||||
spline_order: 4,
|
||||
hidden_width: 32,
|
||||
num_layers: 2,
|
||||
weight_decay: 1e-4,
|
||||
grad_clip: 1.0,
|
||||
batch_size: 32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of continuous parameters in the KAN parameter space.
|
||||
const NUM_PARAMS: usize = 8;
|
||||
|
||||
impl ParameterSpace for KANParams {
|
||||
fn continuous_bounds() -> Vec<(f64, f64)> {
|
||||
vec![
|
||||
(1e-5_f64.ln(), 1e-2_f64.ln()), // learning_rate (log)
|
||||
(3.0, 12.0), // grid_size (linear)
|
||||
(2.0, 5.0), // spline_order (linear)
|
||||
(16.0, 64.0), // hidden_width (linear)
|
||||
(2.0, 4.0), // num_layers (linear)
|
||||
(1e-6_f64.ln(), 1e-2_f64.ln()), // weight_decay (log)
|
||||
(0.5_f64.ln(), 5.0_f64.ln()), // grad_clip (log)
|
||||
(8.0, 64.0), // batch_size (linear)
|
||||
]
|
||||
}
|
||||
|
||||
fn from_continuous(x: &[f64]) -> Result<Self, MLError> {
|
||||
if x.len() != NUM_PARAMS {
|
||||
return Err(MLError::ConfigError {
|
||||
reason: format!("Expected {} continuous parameters, got {}", NUM_PARAMS, x.len()),
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
learning_rate: x.first().copied().unwrap_or(0.0).exp(),
|
||||
grid_size: x.get(1).copied().unwrap_or(5.0).round().max(3.0) as usize,
|
||||
spline_order: x.get(2).copied().unwrap_or(4.0).round().max(2.0) as usize,
|
||||
hidden_width: x.get(3).copied().unwrap_or(32.0).round().max(16.0) as usize,
|
||||
num_layers: x.get(4).copied().unwrap_or(2.0).round().max(2.0) as usize,
|
||||
weight_decay: x.get(5).copied().unwrap_or(0.0).exp(),
|
||||
grad_clip: x.get(6).copied().unwrap_or(0.0).exp(),
|
||||
batch_size: x.get(7).copied().unwrap_or(32.0).round().max(8.0) as usize,
|
||||
})
|
||||
}
|
||||
|
||||
fn to_continuous(&self) -> Vec<f64> {
|
||||
vec![
|
||||
self.learning_rate.ln(),
|
||||
self.grid_size as f64,
|
||||
self.spline_order as f64,
|
||||
self.hidden_width as f64,
|
||||
self.num_layers as f64,
|
||||
self.weight_decay.ln(),
|
||||
self.grad_clip.ln(),
|
||||
self.batch_size as f64,
|
||||
]
|
||||
}
|
||||
|
||||
fn param_names() -> Vec<&'static str> {
|
||||
vec![
|
||||
"learning_rate",
|
||||
"grid_size",
|
||||
"spline_order",
|
||||
"hidden_width",
|
||||
"num_layers",
|
||||
"weight_decay",
|
||||
"grad_clip",
|
||||
"batch_size",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Metrics returned from KAN hyperopt training.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct KANMetrics {
|
||||
/// Validation loss (primary objective for optimization)
|
||||
pub val_loss: f64,
|
||||
/// Training loss at end of run
|
||||
pub train_loss: f64,
|
||||
/// Number of epochs completed
|
||||
pub epochs_completed: usize,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bounds_count_matches_param_names() {
|
||||
let bounds = KANParams::continuous_bounds();
|
||||
let names = KANParams::param_names();
|
||||
assert_eq!(bounds.len(), names.len());
|
||||
assert_eq!(bounds.len(), NUM_PARAMS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_continuous() {
|
||||
let params = KANParams::default();
|
||||
let continuous = params.to_continuous();
|
||||
let restored = KANParams::from_continuous(&continuous).unwrap();
|
||||
assert!(
|
||||
(params.learning_rate - restored.learning_rate).abs() < 1e-6,
|
||||
"lr mismatch: {} vs {}",
|
||||
params.learning_rate,
|
||||
restored.learning_rate
|
||||
);
|
||||
assert_eq!(params.grid_size, restored.grid_size);
|
||||
assert_eq!(params.spline_order, restored.spline_order);
|
||||
assert_eq!(params.hidden_width, restored.hidden_width);
|
||||
assert_eq!(params.num_layers, restored.num_layers);
|
||||
assert!(
|
||||
(params.weight_decay - restored.weight_decay).abs() / params.weight_decay < 1e-6,
|
||||
"weight_decay mismatch"
|
||||
);
|
||||
assert!(
|
||||
(params.grad_clip - restored.grad_clip).abs() < 1e-6,
|
||||
"grad_clip mismatch"
|
||||
);
|
||||
assert_eq!(params.batch_size, restored.batch_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_continuous_wrong_length_errors() {
|
||||
let result = KANParams::from_continuous(&[0.1, 0.2]);
|
||||
assert!(result.is_err());
|
||||
let err_msg = format!("{}", result.unwrap_err());
|
||||
assert!(
|
||||
err_msg.contains("Expected 8"),
|
||||
"Error should mention expected count: {}",
|
||||
err_msg
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bounds_are_valid() {
|
||||
for (i, (min, max)) in KANParams::continuous_bounds().iter().enumerate() {
|
||||
assert!(
|
||||
min < max,
|
||||
"Invalid bounds for param {} ({}): {} >= {}",
|
||||
i,
|
||||
KANParams::param_names().get(i).copied().unwrap_or("?"),
|
||||
min,
|
||||
max
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_within_bounds() {
|
||||
let params = KANParams::default();
|
||||
let continuous = params.to_continuous();
|
||||
let bounds = KANParams::continuous_bounds();
|
||||
let names = KANParams::param_names();
|
||||
for (i, (val, (min, max))) in continuous.iter().zip(bounds.iter()).enumerate() {
|
||||
assert!(
|
||||
*val >= *min && *val <= *max,
|
||||
"Param {} ({}) = {} outside [{}, {}]",
|
||||
i,
|
||||
names.get(i).copied().unwrap_or("?"),
|
||||
val,
|
||||
min,
|
||||
max
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_metrics_default() {
|
||||
let metrics = KANMetrics::default();
|
||||
assert_eq!(metrics.val_loss, 0.0);
|
||||
assert_eq!(metrics.train_loss, 0.0);
|
||||
assert_eq!(metrics.epochs_completed, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_continuous_length() {
|
||||
let params = KANParams::default();
|
||||
let continuous = params.to_continuous();
|
||||
assert_eq!(continuous.len(), NUM_PARAMS);
|
||||
}
|
||||
}
|
||||
@@ -52,14 +52,17 @@
|
||||
pub mod async_data_loader;
|
||||
pub mod continuous_ppo;
|
||||
pub mod dqn;
|
||||
pub mod kan;
|
||||
pub mod liquid;
|
||||
pub mod mamba2;
|
||||
pub mod ppo;
|
||||
pub mod tft;
|
||||
pub mod tggn;
|
||||
pub mod tlob;
|
||||
|
||||
// Re-export adapters for convenience
|
||||
pub use async_data_loader::AsyncDataLoader;
|
||||
pub use kan::{KANMetrics, KANParams};
|
||||
pub use liquid::LiquidParams;
|
||||
pub use continuous_ppo::{ContinuousPPOMetrics, ContinuousPPOParams, ContinuousPPOTrainer};
|
||||
pub use dqn::{DQNMetrics, DQNParams, DQNTrainer};
|
||||
@@ -67,3 +70,4 @@ pub use mamba2::{Mamba2Metrics, Mamba2Params, Mamba2Trainer};
|
||||
pub use ppo::{PPOMetrics, PPOParams, PPOTrainer};
|
||||
pub use tft::{TFTMetrics, TFTParams, TFTTrainer as TFTHyperoptTrainer};
|
||||
pub use tggn::{TGGNMetrics, TGGNParams};
|
||||
pub use tlob::{TLOBMetrics, TLOBParams};
|
||||
|
||||
229
ml/src/hyperopt/adapters/tlob.rs
Normal file
229
ml/src/hyperopt/adapters/tlob.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
//! TLOB (Time Limit Order Book) Hyperparameter Optimization Adapter
|
||||
//!
|
||||
//! Provides `TLOBParams` (implementing `ParameterSpace`) and `TLOBMetrics`
|
||||
//! for hyperparameter optimization of the TLOB model via the unified
|
||||
//! optimization framework.
|
||||
|
||||
use crate::hyperopt::traits::ParameterSpace;
|
||||
use crate::MLError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Hyperparameters for TLOB hyperopt tuning.
|
||||
///
|
||||
/// Controls the TLOB transformer's architecture and training:
|
||||
/// - Architecture: `d_model`, `num_heads`, `num_layers`, `seq_len`
|
||||
/// - Regularization: `dropout`, `weight_decay`, `grad_clip`
|
||||
/// - Training: `learning_rate`, `batch_size`
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct TLOBParams {
|
||||
/// Learning rate for AdamW optimizer (log-scale)
|
||||
pub learning_rate: f64,
|
||||
/// Model / hidden dimension for projection layers
|
||||
pub d_model: usize,
|
||||
/// Number of attention heads
|
||||
pub num_heads: usize,
|
||||
/// Number of transformer/projection layers
|
||||
pub num_layers: usize,
|
||||
/// Sequence length (number of time steps in LOB snapshot)
|
||||
pub seq_len: usize,
|
||||
/// Dropout rate for regularization
|
||||
pub dropout: f64,
|
||||
/// Training batch size
|
||||
pub batch_size: usize,
|
||||
/// L2 weight decay (log-scale)
|
||||
pub weight_decay: f64,
|
||||
/// Maximum gradient norm for clipping (log-scale)
|
||||
pub grad_clip: f64,
|
||||
}
|
||||
|
||||
impl Default for TLOBParams {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
learning_rate: 1e-3,
|
||||
d_model: 128,
|
||||
num_heads: 4,
|
||||
num_layers: 2,
|
||||
seq_len: 128,
|
||||
dropout: 0.1,
|
||||
batch_size: 32,
|
||||
weight_decay: 1e-4,
|
||||
grad_clip: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of continuous parameters in the TLOB parameter space.
|
||||
const NUM_PARAMS: usize = 9;
|
||||
|
||||
impl ParameterSpace for TLOBParams {
|
||||
fn continuous_bounds() -> Vec<(f64, f64)> {
|
||||
vec![
|
||||
(1e-5_f64.ln(), 1e-2_f64.ln()), // learning_rate (log)
|
||||
(64.0, 512.0), // d_model (linear)
|
||||
(2.0, 16.0), // num_heads (linear)
|
||||
(1.0, 8.0), // num_layers (linear)
|
||||
(32.0, 256.0), // seq_len (linear)
|
||||
(0.0, 0.5), // dropout (linear)
|
||||
(8.0, 64.0), // batch_size (linear)
|
||||
(1e-6_f64.ln(), 1e-2_f64.ln()), // weight_decay (log)
|
||||
(0.5_f64.ln(), 5.0_f64.ln()), // grad_clip (log)
|
||||
]
|
||||
}
|
||||
|
||||
fn from_continuous(x: &[f64]) -> Result<Self, MLError> {
|
||||
if x.len() != NUM_PARAMS {
|
||||
return Err(MLError::ConfigError {
|
||||
reason: format!("Expected {} continuous parameters, got {}", NUM_PARAMS, x.len()),
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
learning_rate: x.first().copied().unwrap_or(0.0).exp(),
|
||||
d_model: x.get(1).copied().unwrap_or(128.0).round().max(64.0) as usize,
|
||||
num_heads: x.get(2).copied().unwrap_or(4.0).round().max(2.0) as usize,
|
||||
num_layers: x.get(3).copied().unwrap_or(2.0).round().max(1.0) as usize,
|
||||
seq_len: x.get(4).copied().unwrap_or(128.0).round().max(32.0) as usize,
|
||||
dropout: x.get(5).copied().unwrap_or(0.1).clamp(0.0, 0.5),
|
||||
batch_size: x.get(6).copied().unwrap_or(32.0).round().max(8.0) as usize,
|
||||
weight_decay: x.get(7).copied().unwrap_or(0.0).exp(),
|
||||
grad_clip: x.get(8).copied().unwrap_or(0.0).exp(),
|
||||
})
|
||||
}
|
||||
|
||||
fn to_continuous(&self) -> Vec<f64> {
|
||||
vec![
|
||||
self.learning_rate.ln(),
|
||||
self.d_model as f64,
|
||||
self.num_heads as f64,
|
||||
self.num_layers as f64,
|
||||
self.seq_len as f64,
|
||||
self.dropout,
|
||||
self.batch_size as f64,
|
||||
self.weight_decay.ln(),
|
||||
self.grad_clip.ln(),
|
||||
]
|
||||
}
|
||||
|
||||
fn param_names() -> Vec<&'static str> {
|
||||
vec![
|
||||
"learning_rate",
|
||||
"d_model",
|
||||
"num_heads",
|
||||
"num_layers",
|
||||
"seq_len",
|
||||
"dropout",
|
||||
"batch_size",
|
||||
"weight_decay",
|
||||
"grad_clip",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Metrics returned from TLOB hyperopt training.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct TLOBMetrics {
|
||||
/// Validation loss (primary objective for optimization)
|
||||
pub val_loss: f64,
|
||||
/// Training loss at end of run
|
||||
pub train_loss: f64,
|
||||
/// Directional accuracy on validation set
|
||||
pub directional_accuracy: f64,
|
||||
/// Number of epochs completed
|
||||
pub epochs_completed: usize,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bounds_count_matches_param_names() {
|
||||
let bounds = TLOBParams::continuous_bounds();
|
||||
let names = TLOBParams::param_names();
|
||||
assert_eq!(bounds.len(), names.len());
|
||||
assert_eq!(bounds.len(), NUM_PARAMS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_continuous() {
|
||||
let params = TLOBParams::default();
|
||||
let continuous = params.to_continuous();
|
||||
let restored = TLOBParams::from_continuous(&continuous).unwrap();
|
||||
assert!(
|
||||
(params.learning_rate - restored.learning_rate).abs() < 1e-6,
|
||||
"lr mismatch: {} vs {}",
|
||||
params.learning_rate,
|
||||
restored.learning_rate
|
||||
);
|
||||
assert_eq!(params.d_model, restored.d_model);
|
||||
assert_eq!(params.num_heads, restored.num_heads);
|
||||
assert_eq!(params.num_layers, restored.num_layers);
|
||||
assert_eq!(params.seq_len, restored.seq_len);
|
||||
assert!((params.dropout - restored.dropout).abs() < 1e-6, "dropout mismatch");
|
||||
assert_eq!(params.batch_size, restored.batch_size);
|
||||
assert!(
|
||||
(params.weight_decay - restored.weight_decay).abs() / params.weight_decay < 1e-6,
|
||||
"weight_decay mismatch"
|
||||
);
|
||||
assert!(
|
||||
(params.grad_clip - restored.grad_clip).abs() < 1e-6,
|
||||
"grad_clip mismatch"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_continuous_wrong_length_errors() {
|
||||
let result = TLOBParams::from_continuous(&[0.1, 0.2]);
|
||||
assert!(result.is_err());
|
||||
let err_msg = format!("{}", result.unwrap_err());
|
||||
assert!(err_msg.contains("Expected 9"), "Error should mention expected count: {}", err_msg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bounds_are_valid() {
|
||||
for (i, (min, max)) in TLOBParams::continuous_bounds().iter().enumerate() {
|
||||
assert!(
|
||||
min < max,
|
||||
"Invalid bounds for param {} ({}): {} >= {}",
|
||||
i,
|
||||
TLOBParams::param_names().get(i).copied().unwrap_or("?"),
|
||||
min,
|
||||
max
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_within_bounds() {
|
||||
let params = TLOBParams::default();
|
||||
let continuous = params.to_continuous();
|
||||
let bounds = TLOBParams::continuous_bounds();
|
||||
let names = TLOBParams::param_names();
|
||||
for (i, (val, (min, max))) in continuous.iter().zip(bounds.iter()).enumerate() {
|
||||
assert!(
|
||||
*val >= *min && *val <= *max,
|
||||
"Param {} ({}) = {} outside [{}, {}]",
|
||||
i,
|
||||
names.get(i).copied().unwrap_or("?"),
|
||||
val,
|
||||
min,
|
||||
max
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_metrics_default() {
|
||||
let metrics = TLOBMetrics::default();
|
||||
assert_eq!(metrics.val_loss, 0.0);
|
||||
assert_eq!(metrics.train_loss, 0.0);
|
||||
assert_eq!(metrics.directional_accuracy, 0.0);
|
||||
assert_eq!(metrics.epochs_completed, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_continuous_length() {
|
||||
let params = TLOBParams::default();
|
||||
let continuous = params.to_continuous();
|
||||
assert_eq!(continuous.len(), NUM_PARAMS);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user