Files
foxhunt/ml/tests/rainbow_capacity_tests.rs
jgrusewski 2df1ea92e1 feat(ml): WAVE 29 DQN Codebase Cleanup & Refactoring Campaign
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>
2025-11-27 23:46:13 +01:00

223 lines
7.6 KiB
Rust

#[cfg(test)]
mod rainbow_capacity_tests {
use anyhow::Result;
use candle_core::{DType, Device, Tensor};
use candle_nn::{Module, VarBuilder, VarMap};
use ml::dqn::rainbow_network::{RainbowNetwork, RainbowNetworkConfig};
#[test]
fn test_default_hidden_sizes_reduced() -> Result<()> {
let config = RainbowNetworkConfig::default();
assert_eq!(
config.hidden_sizes,
vec![256, 128],
"Default hidden sizes should be [256, 128] for reduced capacity (was [512, 512])"
);
Ok(())
}
#[test]
fn test_default_dropout_increased() -> Result<()> {
let config = RainbowNetworkConfig::default();
assert!(
(config.dropout_rate - 0.3).abs() < 0.001,
"Default dropout rate should be 0.3 for better regularization (was 0.1)"
);
Ok(())
}
#[test]
fn test_layer_norm_enabled_by_default() -> Result<()> {
let config = RainbowNetworkConfig::default();
assert!(
config.use_layer_norm,
"Layer normalization should be enabled by default for anti-overfitting"
);
assert!(
(config.layer_norm_eps - 1e-5).abs() < 1e-10,
"Layer norm epsilon should be 1e-5"
);
Ok(())
}
#[test]
fn test_network_forward_pass_with_reduced_capacity() -> Result<()> {
// Ensure network still functions correctly with reduced capacity
let device = Device::Cpu;
let varmap = VarMap::new();
let vs = VarBuilder::from_varmap(&varmap, DType::F32, &device);
let config = RainbowNetworkConfig {
input_size: 64,
num_actions: 3,
hidden_sizes: vec![256, 128],
dropout_rate: 0.3,
..Default::default()
};
let network = RainbowNetwork::new(&vs, config)
.map_err(|e| anyhow::anyhow!("Failed to create network: {}", e))?;
let state = Tensor::randn(0.0f32, 1.0, (1, 64), &device)?;
let output = network.forward(&state)?;
// Output should be [batch_size, num_actions, num_atoms]
let output_shape = output.shape().dims();
assert_eq!(
output_shape,
&[1, 3, 51],
"Output shape should be [1, 3, 51] for batch=1, actions=3, atoms=51"
);
Ok(())
}
#[test]
fn test_capacity_comparison_with_legacy() -> Result<()> {
// Compare new reduced capacity with old larger network
let device = Device::Cpu;
// Reduced capacity network
let reduced_varmap = VarMap::new();
let reduced_vs = VarBuilder::from_varmap(&reduced_varmap, DType::F32, &device);
let reduced_config = RainbowNetworkConfig {
input_size: 64,
num_actions: 3,
hidden_sizes: vec![256, 128], // New reduced
dropout_rate: 0.3,
..Default::default()
};
let _reduced_net = RainbowNetwork::new(&reduced_vs, reduced_config.clone())
.map_err(|e| anyhow::anyhow!("Failed to create reduced network: {}", e))?;
// Legacy larger network
let legacy_varmap = VarMap::new();
let legacy_vs = VarBuilder::from_varmap(&legacy_varmap, DType::F32, &device);
let legacy_config = RainbowNetworkConfig {
input_size: 64,
num_actions: 3,
hidden_sizes: vec![512, 512], // Old larger
dropout_rate: 0.1,
..Default::default()
};
let _legacy_net = RainbowNetwork::new(&legacy_vs, legacy_config.clone())
.map_err(|e| anyhow::anyhow!("Failed to create legacy network: {}", e))?;
// Count parameters (approximate calculation)
let reduced_params = estimate_parameter_count(&reduced_config);
let legacy_params = estimate_parameter_count(&legacy_config);
assert!(
reduced_params < legacy_params,
"Reduced network ({} params) should have fewer parameters than legacy ({} params)",
reduced_params,
legacy_params
);
// Should be roughly 3-4x reduction
let ratio = legacy_params as f64 / reduced_params as f64;
assert!(
ratio > 2.0 && ratio < 5.0,
"Parameter reduction ratio should be 2-5x, got {:.2}x",
ratio
);
println!("✓ Reduced network: {} params", reduced_params);
println!("✓ Legacy network: {} params", legacy_params);
println!("✓ Reduction ratio: {:.2}x", ratio);
Ok(())
}
#[test]
fn test_parameter_count_reasonable() -> Result<()> {
// Verify total params < 500K for 64-dim input, 3 actions, 51 atoms
let config = RainbowNetworkConfig {
input_size: 64,
num_actions: 3,
hidden_sizes: vec![256, 128],
dropout_rate: 0.3,
..Default::default()
};
let param_count = estimate_parameter_count(&config);
assert!(
param_count < 500_000,
"Parameter count {} should be < 500K to prevent overfitting",
param_count
);
println!("✓ Rainbow network parameter count: {}", param_count);
Ok(())
}
#[test]
fn test_dueling_architecture_enabled() -> Result<()> {
let config = RainbowNetworkConfig::default();
assert!(
config.dueling,
"Dueling architecture should be enabled by default"
);
Ok(())
}
#[test]
fn test_noisy_layers_enabled() -> Result<()> {
let config = RainbowNetworkConfig::default();
assert!(
config.use_noisy_layers,
"Noisy layers should be enabled by default for exploration"
);
Ok(())
}
/// Estimate parameter count for Rainbow network
/// This is an approximation of the actual parameter count
fn estimate_parameter_count(config: &RainbowNetworkConfig) -> usize {
let mut total_params = 0;
// Feature extraction layers
let mut current_size = config.input_size;
for &hidden_size in &config.hidden_sizes {
// Linear layer: (input * output) + bias
total_params += current_size * hidden_size + hidden_size;
// LayerNorm: 2 * hidden_size (weight + bias)
if config.use_layer_norm {
total_params += 2 * hidden_size;
}
current_size = hidden_size;
}
let final_feature_size = current_size;
let num_atoms = config.distributional.num_atoms;
if config.dueling {
// Value stream
let value_hidden = final_feature_size / 2;
total_params += final_feature_size * value_hidden + value_hidden;
if config.use_layer_norm {
total_params += 2 * value_hidden;
}
// Value distribution
total_params += value_hidden * num_atoms + num_atoms;
// Advantage stream
let advantage_hidden = final_feature_size / 2;
total_params += final_feature_size * advantage_hidden + advantage_hidden;
if config.use_layer_norm {
total_params += 2 * advantage_hidden;
}
// Advantage distribution
let advantage_output = config.num_actions * num_atoms;
total_params += advantage_hidden * advantage_output + advantage_output;
} else {
// Single action distribution output
let output_size = config.num_actions * num_atoms;
total_params += final_feature_size * output_size + output_size;
}
total_params
}
}