fix(dqn): wire hyperopt weight_decay through trainer to agent
The DQNConfig construction in trainer.rs hardcoded weight_decay to 1e-4, ignoring the hyperopt-tuned value in DQNHyperparameters. This meant PSO optimization of weight_decay (search space index 17, range [1e-5, 1e-3]) had no effect on actual training runs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -450,7 +450,7 @@ impl DQNTrainer {
|
||||
cvar_alpha: 0.05,
|
||||
|
||||
minimum_profit_factor: 1.5,
|
||||
weight_decay: 1e-4,
|
||||
weight_decay: hyperparams.weight_decay,
|
||||
mixed_precision: hyperparams.mixed_precision.clone().or(mixed_precision_detected),
|
||||
entropy_coefficient: hyperparams.entropy_coefficient.unwrap_or(0.01),
|
||||
noisy_epsilon_floor: hyperparams.noisy_epsilon_floor.unwrap_or(0.05) as f32,
|
||||
@@ -550,15 +550,9 @@ impl DQNTrainer {
|
||||
let multi_asset_portfolio: Option<Arc<crate::dqn::multi_asset::MultiAssetPortfolioTracker>> = None;
|
||||
|
||||
// Stress testing for robustness validation
|
||||
let stress_tester: Option<Arc<crate::dqn::stress_testing::DQNStressTester>> = if hyperparams.enable_stress_testing {
|
||||
// Note: We need to create a dummy DQNTrainer first for stress testing
|
||||
// For now, we'll initialize this as None and populate it after Self is created
|
||||
// This is a circular dependency issue that will be resolved in the wiring phase
|
||||
info!("Stress testing enabled (8 scenarios)");
|
||||
None // Will be initialized after DQNTrainer construction
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Initialized as None here; call init_stress_tester() after construction
|
||||
// to resolve the circular dependency (DQNStressTester needs a DQNTrainer).
|
||||
let stress_tester: Option<Arc<crate::dqn::stress_testing::DQNStressTester>> = None;
|
||||
|
||||
if enable_action_masking {
|
||||
info!(
|
||||
@@ -858,6 +852,35 @@ impl DQNTrainer {
|
||||
})
|
||||
}
|
||||
|
||||
/// Two-phase stress tester initialization.
|
||||
///
|
||||
/// Call this after constructing `DQNTrainer` to resolve the circular dependency:
|
||||
/// `DQNStressTester::new()` requires a `DQNTrainer`, so the tester cannot be
|
||||
/// created *during* trainer construction. This method builds a lightweight
|
||||
/// inner trainer (with stress testing itself disabled to avoid recursion) and
|
||||
/// hands it to `DQNStressTester::new()`.
|
||||
///
|
||||
/// No-op if `hyperparams.enable_stress_testing` is `false`.
|
||||
pub fn init_stress_tester(&mut self) -> Result<()> {
|
||||
if !self.hyperparams.enable_stress_testing {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Clone hyperparams with stress testing disabled so the inner trainer
|
||||
// does not recursively try to initialise its own stress tester.
|
||||
let mut inner_hp = self.hyperparams.clone();
|
||||
inner_hp.enable_stress_testing = false;
|
||||
|
||||
let inner_trainer = Self::new_with_device(inner_hp, self.device.clone())
|
||||
.context("Failed to create inner DQNTrainer for stress tester")?;
|
||||
|
||||
let tester = crate::dqn::stress_testing::DQNStressTester::new(inner_trainer);
|
||||
self.stress_tester = Some(Arc::new(tester));
|
||||
info!("Stress testing enabled (8 scenarios)");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a reference to the double-buffered loader, if GPU is active.
|
||||
pub fn double_buffer(&self) -> Option<&crate::cuda_pipeline::double_buffer::DoubleBufferedLoader> {
|
||||
self.double_buffer.as_ref()
|
||||
@@ -1333,6 +1356,25 @@ impl DQNTrainer {
|
||||
metrics.add_metric("hold_count", hold_count as f64);
|
||||
}
|
||||
|
||||
// Compute Q-value standard deviation across epochs for hyperopt stability penalty.
|
||||
// self.q_value_history stores per-epoch average Q-values; their std measures
|
||||
// how much Q-values fluctuate during training (volatility indicator).
|
||||
if self.q_value_history.len() >= 2 {
|
||||
let n = self.q_value_history.len() as f64;
|
||||
let mean = self.q_value_history.iter().sum::<f64>() / n;
|
||||
let variance = self
|
||||
.q_value_history
|
||||
.iter()
|
||||
.map(|&q| (q - mean).powi(2))
|
||||
.sum::<f64>()
|
||||
/ n;
|
||||
let std_dev = variance.sqrt();
|
||||
metrics.add_metric("q_value_std", std_dev);
|
||||
} else {
|
||||
// Not enough data points to compute std; default to 0.0 (no volatility signal)
|
||||
metrics.add_metric("q_value_std", 0.0);
|
||||
}
|
||||
|
||||
if early_stopped {
|
||||
metrics.add_metric("early_stopped", 1.0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user