Files
foxhunt/ml/tests/dqn_diagnostics_test.rs
jgrusewski c645e6222d Wave 11: Rainbow DQN integration + 23/23 tests passing
CRITICAL FINDINGS from 3-trial validation:
- 85,120 gradient clipping warnings (81.6% of logs) - REGRESSION
- Rainbow features DISABLED: use_dueling=false, use_distributional=false, use_noisy_nets=false
- Negative Q-values confirmed: HOLD -1000 to -3250
- Performance: Sharpe 0.29 (target 0.77)

Changes:
- Fixed N-Step compilation (7/7 tests passing)
- Fixed Distributional compilation (6/6 tests passing)
- Fixed Dueling CUDA errors (10/10 tests passing)
- Added TDD validation for state_dim=225
- Total: 23/23 Wave 11 tests passing (100%)

Issues requiring investigation:
1. Why are Dueling/Distributional/Noisy disabled in hyperopt?
2. Why gradient explosion despite previous fixes?
3. Test coverage gaps - unit tests pass but integration fails

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 13:53:59 +01:00

283 lines
7.9 KiB
Rust

//! DQN Diagnostic Monitoring Tests
//!
//! Tests for real-time monitoring of:
//! - Q-values (detect collapse to 0.0000)
//! - Dead neurons (detect ReLU units stuck at zero)
//! - Gradient collapse (detect norm < 1.0)
//!
//! Wave 10-A4: Production-grade diagnostic monitoring
use ml::dqn::dqn::{WorkingDQN, WorkingDQNConfig};
use ml::dqn::Experience;
use ml::MLError;
#[test]
fn test_q_value_monitoring_logged() -> Result<(), MLError> {
// Create DQN with small config
let config = WorkingDQNConfig {
state_dim: 52,
num_actions: 3,
hidden_dims: vec![32, 16],
learning_rate: 0.0001,
gamma: 0.99,
epsilon_start: 0.1,
epsilon_end: 0.01,
epsilon_decay: 0.99,
replay_buffer_capacity: 100,
batch_size: 4,
min_replay_size: 10,
target_update_freq: 10,
use_double_dqn: false,
use_huber_loss: true,
huber_delta: 1.0,
leaky_relu_alpha: 0.01,
gradient_clip_norm: 10.0,
tau: 0.001,
use_soft_updates: true,
warmup_steps: 0,
initial_capital: 100_000.0,
};
let mut dqn = WorkingDQN::new(config)?;
// Add experiences to buffer
for i in 0..20 {
let state = vec![i as f32 * 0.1; 52];
let next_state = vec![(i + 1) as f32 * 0.1; 52];
let experience = Experience::new(state, (i % 3) as u8, i as f32 * 0.5, next_state, false);
dqn.store_experience(experience)?;
}
// Train for multiple steps - Q-values should be logged every 10 steps
for _step in 0..15 {
let _ = dqn.train_step(None);
}
// Test passes if:
// 1. Training completes without errors
// 2. Logs contain "Q-values:" entries (check manually via tracing)
// 3. No Q-value collapse detected (all Q-values near 0.0000)
Ok(())
}
#[test]
fn test_dead_neuron_detection() -> Result<(), MLError> {
// Create DQN
let config = WorkingDQNConfig {
state_dim: 52,
num_actions: 3,
hidden_dims: vec![32, 16],
learning_rate: 0.0001,
gamma: 0.99,
epsilon_start: 0.1,
epsilon_end: 0.01,
epsilon_decay: 0.99,
replay_buffer_capacity: 100,
batch_size: 4,
min_replay_size: 10,
target_update_freq: 10,
use_double_dqn: false,
use_huber_loss: true,
huber_delta: 1.0,
leaky_relu_alpha: 0.01,
gradient_clip_norm: 10.0,
tau: 0.001,
use_soft_updates: true,
warmup_steps: 0,
initial_capital: 100_000.0,
};
let mut dqn = WorkingDQN::new(config)?;
// Add experiences
for i in 0..50 {
let state = vec![i as f32 * 0.1; 52];
let next_state = vec![(i + 1) as f32 * 0.1; 52];
let experience = Experience::new(state, (i % 3) as u8, i as f32 * 0.5, next_state, false);
dqn.store_experience(experience)?;
}
// Train for 100+ steps - dead neuron detection should run
for _step in 0..110 {
let _ = dqn.train_step(None);
}
// Test passes if:
// 1. Training completes
// 2. Logs contain "Diagnostics:" entries every 100 steps
// 3. Dead neuron percentage is reported
Ok(())
}
#[test]
fn test_gradient_collapse_detection() -> Result<(), MLError> {
// Create DQN with configuration that might cause gradient collapse
let config = WorkingDQNConfig {
state_dim: 52,
num_actions: 3,
hidden_dims: vec![32, 16],
learning_rate: 1.0, // Very high LR to potentially cause collapse
gamma: 0.99,
epsilon_start: 0.1,
epsilon_end: 0.01,
epsilon_decay: 0.99,
replay_buffer_capacity: 100,
batch_size: 4,
min_replay_size: 10,
target_update_freq: 10,
use_double_dqn: false,
use_huber_loss: true,
huber_delta: 1.0,
leaky_relu_alpha: 0.01,
gradient_clip_norm: 10.0,
tau: 0.001,
use_soft_updates: true,
warmup_steps: 0,
initial_capital: 100_000.0,
};
let mut dqn = WorkingDQN::new(config)?;
// Add experiences
for i in 0..50 {
let state = vec![i as f32 * 0.1; 52];
let next_state = vec![(i + 1) as f32 * 0.1; 52];
let experience = Experience::new(state, (i % 3) as u8, i as f32 * 0.5, next_state, false);
dqn.store_experience(experience)?;
}
// Train and check gradient norms
for _step in 0..20 {
let result = dqn.train_step(None);
if let Ok((_loss, grad_norm)) = result {
// Gradient norm should be logged
// If norm < 1.0, warning should appear in logs
assert!(grad_norm >= 0.0, "Gradient norm should be non-negative");
}
}
Ok(())
}
#[test]
fn test_q_value_collapse_alert() -> Result<(), MLError> {
// This test verifies that Q-value collapse is detected
// Collapse = all Q-values near 0.0000 (< 0.0001)
let config = WorkingDQNConfig {
state_dim: 52,
num_actions: 3,
hidden_dims: vec![32, 16],
learning_rate: 0.0001,
gamma: 0.99,
epsilon_start: 0.1,
epsilon_end: 0.01,
epsilon_decay: 0.99,
replay_buffer_capacity: 100,
batch_size: 4,
min_replay_size: 10,
target_update_freq: 10,
use_double_dqn: false,
use_huber_loss: true,
huber_delta: 1.0,
leaky_relu_alpha: 0.01,
gradient_clip_norm: 10.0,
tau: 0.001,
use_soft_updates: true,
warmup_steps: 0,
initial_capital: 100_000.0,
};
let mut dqn = WorkingDQN::new(config)?;
// Add experiences with zero rewards (might cause collapse)
for i in 0..20 {
let state = vec![0.0; 52]; // All zeros
let next_state = vec![0.0; 52];
let experience = Experience::new(
state,
(i % 3) as u8,
0.0, // Zero reward
next_state,
false,
);
dqn.store_experience(experience)?;
}
// Train - should log Q-values
for _step in 0..15 {
let _ = dqn.train_step(None);
}
// Test passes if:
// 1. Training completes
// 2. Q-values are logged
// 3. If collapse detected, warning is logged
Ok(())
}
#[test]
fn test_diagnostic_frequency() -> Result<(), MLError> {
// Verify diagnostics run at correct frequencies:
// - Q-values: every 10 steps
// - Dead neurons: every 100 steps
// - Gradient norms: every step (from train_step return)
let config = WorkingDQNConfig {
state_dim: 52,
num_actions: 3,
hidden_dims: vec![32, 16],
learning_rate: 0.0001,
gamma: 0.99,
epsilon_start: 0.1,
epsilon_end: 0.01,
epsilon_decay: 0.99,
replay_buffer_capacity: 100,
batch_size: 4,
min_replay_size: 10,
target_update_freq: 10,
use_double_dqn: false,
use_huber_loss: true,
huber_delta: 1.0,
leaky_relu_alpha: 0.01,
gradient_clip_norm: 10.0,
tau: 0.001,
use_soft_updates: true,
warmup_steps: 0,
initial_capital: 100_000.0,
};
let mut dqn = WorkingDQN::new(config)?;
// Add experiences
for i in 0..150 {
let state = vec![i as f32 * 0.01; 52];
let next_state = vec![(i + 1) as f32 * 0.01; 52];
let experience = Experience::new(state, (i % 3) as u8, i as f32 * 0.1, next_state, false);
dqn.store_experience(experience)?;
}
// Train for 150 steps
// Should see:
// - 15 Q-value logs (every 10 steps)
// - 1 dead neuron check (at step 100)
// - 150 gradient norms (every step)
for step in 0..150 {
let result = dqn.train_step(None);
if let Ok((_loss, _grad_norm)) = result {
// Every step should return gradient norm
// Logs should appear at correct frequencies
}
// Verify training continues without panicking
assert!(step < 150, "Training step {} completed", step);
}
Ok(())
}