BREAKING CHANGES: - Removed orphaned dqn.rs monolithic trainer (4,975 lines) - Removed orphaned dqn_ensemble.rs module (816 lines) - Removed orphaned tft.rs and tft_complete_int8_integration_test.rs - TFT trainer split into modular directory structure DQN Module Refactoring: - Split trainers/dqn.rs into modular structure (config.rs, statistics.rs, trainer.rs) - Fixed hyperopt 39D search space (continuous params only) - Boolean flags (use_dueling, use_double_dqn, use_per, use_noisy_nets) are now FIXED architectural decisions - use_distributional defaults to false (Candle BUG #36 - scatter_add gradient issues) Clean Module Structure: - ml/src/trainers/dqn/ directory with proper mod.rs exports - ml/src/trainers/tft/ directory with config.rs, types.rs, model.rs, trainer.rs, tests.rs - All P0 features validated: TD-error clamping, batch diversity, LR scheduler, priority staleness Documentation: - Added comprehensive docs in docs/codebase-cleanup/ - ADR-001 for DQN refactoring decisions - Rainbow DQN component matrix and quick reference guides Build Status: Compiles with zero errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
135 lines
4.3 KiB
Rust
135 lines
4.3 KiB
Rust
//! DQN Training Statistics
|
||
//!
|
||
//! Feature normalization and Q-value monitoring for DQN training.
|
||
//!
|
||
//! This module provides:
|
||
//! - `FeatureStatistics`: Welford's online algorithm for numerically stable feature normalization
|
||
//! - `QValueStats`: Q-value distribution tracking for adaptive C51 bounds
|
||
|
||
/// Feature statistics for online normalization using Welford's algorithm
|
||
///
|
||
/// WAVE 1.2 P1 Fix: Numerical Stability via Welford's Algorithm
|
||
///
|
||
/// Problem: Standard E[X²] - E[X]² variance calculation causes catastrophic cancellation
|
||
/// when feature values are large (e.g., 1e9+), making normalization impossible and
|
||
/// predictions diverge (MSE → inf, portfolio value → 0).
|
||
///
|
||
/// Solution: Welford's online variance algorithm (Knuth, Vol 2, 1998):
|
||
/// ```
|
||
/// δ = x - mean
|
||
/// mean += δ / count
|
||
/// δ2 = x - mean (using NEW mean!)
|
||
/// M2 += δ * δ2
|
||
/// variance = M2 / count
|
||
/// ```
|
||
///
|
||
/// Benefits:
|
||
/// - No catastrophic cancellation (avoids E[X²] - E[X]² subtraction)
|
||
/// - Single pass through data (online algorithm)
|
||
/// - Numerically stable for any magnitude values
|
||
/// - Memory efficient (only stores mean and M2, not all samples)
|
||
///
|
||
/// Numerical Stability:
|
||
/// - Welford's algorithm avoids catastrophic cancellation (σ² = E[X²] - E[X]² breaks for large values)
|
||
/// - Single pass through data (no need to store all samples)
|
||
/// - Handles large values (1e9+) without precision loss
|
||
///
|
||
/// Expected Impact: +55-94% Sharpe improvement (most impactful P1 fix)
|
||
#[derive(Clone, Debug)]
|
||
pub struct FeatureStatistics {
|
||
/// Number of samples seen
|
||
pub count: usize,
|
||
/// Running mean for each feature (f64 for precision)
|
||
pub mean: Vec<f64>,
|
||
/// Sum of squared differences from mean (Welford's M2)
|
||
pub m2: Vec<f64>,
|
||
}
|
||
|
||
impl FeatureStatistics {
|
||
/// Create new feature statistics tracker
|
||
pub fn new(num_features: usize) -> Self {
|
||
Self {
|
||
count: 0,
|
||
mean: vec![0.0; num_features],
|
||
m2: vec![0.0; num_features],
|
||
}
|
||
}
|
||
|
||
/// Update statistics with new sample using Welford's algorithm
|
||
///
|
||
/// Welford's online algorithm (single pass, numerically stable):
|
||
/// ```
|
||
/// δ = x - mean
|
||
/// mean += δ / count
|
||
/// δ2 = x - mean (new mean!)
|
||
/// M2 += δ * δ2
|
||
/// ```
|
||
pub fn update(&mut self, features: &[f32]) {
|
||
self.count += 1;
|
||
for (i, &value) in features.iter().enumerate() {
|
||
let delta = value as f64 - self.mean[i];
|
||
self.mean[i] += delta / self.count as f64;
|
||
let delta2 = value as f64 - self.mean[i];
|
||
self.m2[i] += delta * delta2;
|
||
}
|
||
}
|
||
|
||
/// Compute standard deviation from M2
|
||
pub fn std_dev(&self) -> Vec<f64> {
|
||
self.m2
|
||
.iter()
|
||
.map(|&m2| (m2 / self.count as f64).sqrt())
|
||
.collect()
|
||
}
|
||
|
||
/// Normalize features to z-scores: z = (x - μ) / σ
|
||
pub fn normalize(&self, features: &[f32]) -> Vec<f32> {
|
||
let std_dev = self.std_dev();
|
||
features
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, &value)| {
|
||
let std = std_dev[i];
|
||
if std < 1e-8 { 0.0 } else { ((value as f64 - self.mean[i]) / std) as f32 }
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// Normalize features with placeholder skipping
|
||
///
|
||
/// Skips normalization for specified indices (e.g., portfolio placeholders at 125-127)
|
||
/// Placeholders remain 0.0 to avoid breaking downstream logic
|
||
pub fn normalize_with_skip(&self, features: &[f32], skip_indices: &[usize]) -> Vec<f32> {
|
||
let std_dev = self.std_dev();
|
||
features
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, &value)| {
|
||
// Skip normalization for placeholders
|
||
if skip_indices.contains(&i) {
|
||
value
|
||
} else {
|
||
let std = std_dev[i];
|
||
if std < 1e-8 { 0.0 } else { ((value as f64 - self.mean[i]) / std) as f32 }
|
||
}
|
||
})
|
||
.collect()
|
||
}
|
||
}
|
||
|
||
/// Q-Value statistics for adaptive C51 bounds
|
||
#[derive(Clone, Debug)]
|
||
pub struct QValueStats {
|
||
pub min: f64,
|
||
pub max: f64,
|
||
pub mean: f64,
|
||
pub std: f64,
|
||
pub sample_count: usize,
|
||
}
|
||
|
||
impl QValueStats {
|
||
pub fn range(&self) -> f64 {
|
||
self.max - self.min
|
||
}
|
||
}
|