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>
343 lines
9.3 KiB
Rust
343 lines
9.3 KiB
Rust
//! Continuous PPO Trainer Tests
|
|
//!
|
|
//! Integration tests for continuous action space PPO training.
|
|
|
|
use anyhow::Result;
|
|
use ml::ppo::continuous_ppo::{
|
|
ContinuousAction, ContinuousPPO, ContinuousPPOConfig, ContinuousTrajectory,
|
|
ContinuousTrajectoryBatch, ContinuousTrajectoryStep,
|
|
};
|
|
use ml::ppo::continuous_policy::ContinuousPolicyConfig;
|
|
use ml::ppo::gae::GAEConfig;
|
|
|
|
#[test]
|
|
fn test_trainer_continuous_ppo_creation() -> Result<()> {
|
|
// Create continuous PPO config
|
|
let policy_config = ContinuousPolicyConfig {
|
|
state_dim: 64,
|
|
hidden_dims: vec![128, 64],
|
|
min_log_std: -5.0,
|
|
max_log_std: 2.0,
|
|
init_log_std: -1.0,
|
|
learnable_std: true,
|
|
action_bounds: (-1.0, 1.0),
|
|
};
|
|
|
|
let config = ContinuousPPOConfig {
|
|
state_dim: 64,
|
|
policy_config,
|
|
value_hidden_dims: vec![128, 64],
|
|
policy_learning_rate: 0.000001,
|
|
value_learning_rate: 0.001,
|
|
clip_epsilon: 0.2,
|
|
value_loss_coeff: 0.5,
|
|
entropy_coeff: 0.01,
|
|
gae_config: GAEConfig {
|
|
gamma: 0.99,
|
|
lambda: 0.95,
|
|
normalize_advantages: true,
|
|
},
|
|
batch_size: 64,
|
|
mini_batch_size: 16,
|
|
num_epochs: 5,
|
|
max_grad_norm: 0.5,
|
|
};
|
|
|
|
// Create agent
|
|
let agent = ContinuousPPO::new(config)?;
|
|
|
|
// Verify initialization
|
|
assert_eq!(agent.get_training_steps(), 0);
|
|
assert_eq!(agent.get_config().state_dim, 64);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_trainer_continuous_training_step() -> Result<()> {
|
|
// Create continuous PPO config
|
|
let policy_config = ContinuousPolicyConfig {
|
|
state_dim: 10,
|
|
hidden_dims: vec![16, 8],
|
|
min_log_std: -5.0,
|
|
max_log_std: 2.0,
|
|
init_log_std: -1.0,
|
|
learnable_std: true,
|
|
action_bounds: (-1.0, 1.0),
|
|
};
|
|
|
|
let config = ContinuousPPOConfig {
|
|
state_dim: 10,
|
|
policy_config,
|
|
value_hidden_dims: vec![16, 8],
|
|
policy_learning_rate: 0.000001,
|
|
value_learning_rate: 0.001,
|
|
clip_epsilon: 0.2,
|
|
value_loss_coeff: 0.5,
|
|
entropy_coeff: 0.01,
|
|
gae_config: GAEConfig {
|
|
gamma: 0.99,
|
|
lambda: 0.95,
|
|
normalize_advantages: true,
|
|
},
|
|
batch_size: 32,
|
|
mini_batch_size: 8,
|
|
num_epochs: 2,
|
|
max_grad_norm: 0.5,
|
|
};
|
|
|
|
// Create agent
|
|
let mut agent = ContinuousPPO::new(config)?;
|
|
|
|
// Create simple trajectory
|
|
let mut trajectory = ContinuousTrajectory::new();
|
|
|
|
for i in 0..10 {
|
|
let state = vec![0.1 * i as f32; 10];
|
|
let action = ContinuousAction::new(0.5);
|
|
let log_prob = -1.0;
|
|
let reward = 1.0;
|
|
let value = 0.5;
|
|
let done = i == 9;
|
|
|
|
let step =
|
|
ContinuousTrajectoryStep::new(state, action, log_prob, reward, value, done);
|
|
trajectory.add_step(step);
|
|
}
|
|
|
|
// Compute GAE advantages
|
|
let steps = trajectory.steps();
|
|
let rewards: Vec<f32> = steps.iter().map(|s| s.reward).collect();
|
|
let values: Vec<f32> = steps.iter().map(|s| s.value).collect();
|
|
let dones: Vec<bool> = steps.iter().map(|s| s.done).collect();
|
|
|
|
let advantages = compute_gae_advantages(&rewards, &values, &dones, 0.99, 0.95);
|
|
let returns = compute_returns(&rewards, 0.99);
|
|
|
|
// Create batch
|
|
let mut batch = ContinuousTrajectoryBatch::from_trajectories(
|
|
vec![trajectory],
|
|
advantages,
|
|
returns,
|
|
);
|
|
|
|
// Training step
|
|
let initial_steps = agent.get_training_steps();
|
|
let (policy_loss, value_loss) = agent.update(&mut batch)?;
|
|
|
|
// Verify training occurred
|
|
assert_eq!(agent.get_training_steps(), initial_steps + 1);
|
|
assert!(policy_loss.is_finite());
|
|
assert!(value_loss.is_finite());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_trainer_continuous_epoch_completion() -> Result<()> {
|
|
// Create continuous PPO config
|
|
let policy_config = ContinuousPolicyConfig {
|
|
state_dim: 8,
|
|
hidden_dims: vec![16],
|
|
min_log_std: -5.0,
|
|
max_log_std: 2.0,
|
|
init_log_std: -1.0,
|
|
learnable_std: true,
|
|
action_bounds: (-1.0, 1.0),
|
|
};
|
|
|
|
let config = ContinuousPPOConfig {
|
|
state_dim: 8,
|
|
policy_config,
|
|
value_hidden_dims: vec![16],
|
|
policy_learning_rate: 0.000001,
|
|
value_learning_rate: 0.001,
|
|
clip_epsilon: 0.2,
|
|
value_loss_coeff: 0.5,
|
|
entropy_coeff: 0.01,
|
|
gae_config: GAEConfig {
|
|
gamma: 0.99,
|
|
lambda: 0.95,
|
|
normalize_advantages: true,
|
|
},
|
|
batch_size: 32,
|
|
mini_batch_size: 8,
|
|
num_epochs: 3,
|
|
max_grad_norm: 0.5,
|
|
};
|
|
|
|
// Create agent
|
|
let mut agent = ContinuousPPO::new(config)?;
|
|
|
|
// Create multiple trajectories
|
|
let mut trajectories = Vec::new();
|
|
|
|
for traj_idx in 0..3 {
|
|
let mut trajectory = ContinuousTrajectory::new();
|
|
|
|
for i in 0..10 {
|
|
let state = vec![0.1 * (traj_idx * 10 + i) as f32; 8];
|
|
let action = ContinuousAction::new(0.3 + traj_idx as f32 * 0.2);
|
|
let log_prob = -1.5;
|
|
let reward = 0.5 + i as f32 * 0.1;
|
|
let value = 0.3;
|
|
let done = i == 9;
|
|
|
|
let step =
|
|
ContinuousTrajectoryStep::new(state, action, log_prob, reward, value, done);
|
|
trajectory.add_step(step);
|
|
}
|
|
|
|
trajectories.push(trajectory);
|
|
}
|
|
|
|
// Compute batch advantages
|
|
let mut all_advantages = Vec::new();
|
|
let mut all_returns = Vec::new();
|
|
|
|
for trajectory in &trajectories {
|
|
let steps = trajectory.steps();
|
|
let rewards: Vec<f32> = steps.iter().map(|s| s.reward).collect();
|
|
let values: Vec<f32> = steps.iter().map(|s| s.value).collect();
|
|
let dones: Vec<bool> = steps.iter().map(|s| s.done).collect();
|
|
|
|
let advantages = compute_gae_advantages(&rewards, &values, &dones, 0.99, 0.95);
|
|
let returns = compute_returns(&rewards, 0.99);
|
|
|
|
all_advantages.extend(advantages);
|
|
all_returns.extend(returns);
|
|
}
|
|
|
|
// Create batch
|
|
let mut batch = ContinuousTrajectoryBatch::from_trajectories(
|
|
trajectories,
|
|
all_advantages,
|
|
all_returns,
|
|
);
|
|
|
|
// Run multiple training steps
|
|
for epoch in 0..3 {
|
|
let (policy_loss, value_loss) = agent.update(&mut batch)?;
|
|
|
|
assert!(policy_loss.is_finite(), "Policy loss NaN at epoch {}", epoch);
|
|
assert!(value_loss.is_finite(), "Value loss NaN at epoch {}", epoch);
|
|
}
|
|
|
|
assert_eq!(agent.get_training_steps(), 3);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_trainer_continuous_checkpoint_save() -> Result<()> {
|
|
use tempfile::TempDir;
|
|
|
|
// Create temporary directory for checkpoints
|
|
let temp_dir = TempDir::new()?;
|
|
let checkpoint_dir = temp_dir.path();
|
|
|
|
// Create continuous PPO config
|
|
let policy_config = ContinuousPolicyConfig {
|
|
state_dim: 10,
|
|
hidden_dims: vec![16],
|
|
min_log_std: -5.0,
|
|
max_log_std: 2.0,
|
|
init_log_std: -1.0,
|
|
learnable_std: true,
|
|
action_bounds: (-1.0, 1.0),
|
|
};
|
|
|
|
let config = ContinuousPPOConfig {
|
|
state_dim: 10,
|
|
policy_config,
|
|
value_hidden_dims: vec![16],
|
|
policy_learning_rate: 0.000001,
|
|
value_learning_rate: 0.001,
|
|
clip_epsilon: 0.2,
|
|
value_loss_coeff: 0.5,
|
|
entropy_coeff: 0.01,
|
|
gae_config: GAEConfig {
|
|
gamma: 0.99,
|
|
lambda: 0.95,
|
|
normalize_advantages: true,
|
|
},
|
|
batch_size: 32,
|
|
mini_batch_size: 8,
|
|
num_epochs: 2,
|
|
max_grad_norm: 0.5,
|
|
};
|
|
|
|
// Create agent
|
|
let agent = ContinuousPPO::new(config)?;
|
|
|
|
// Save checkpoints
|
|
let actor_path = checkpoint_dir.join("actor_test.safetensors");
|
|
let critic_path = checkpoint_dir.join("critic_test.safetensors");
|
|
|
|
agent
|
|
.actor
|
|
.vars()
|
|
.save(&actor_path)
|
|
.expect("Failed to save actor");
|
|
agent
|
|
.critic
|
|
.vars()
|
|
.save(&critic_path)
|
|
.expect("Failed to save critic");
|
|
|
|
// Verify checkpoint files exist
|
|
assert!(actor_path.exists(), "Actor checkpoint not created");
|
|
assert!(critic_path.exists(), "Critic checkpoint not created");
|
|
|
|
// Verify files have non-zero size
|
|
let actor_metadata = std::fs::metadata(&actor_path)?;
|
|
let critic_metadata = std::fs::metadata(&critic_path)?;
|
|
|
|
assert!(actor_metadata.len() > 0, "Actor checkpoint is empty");
|
|
assert!(critic_metadata.len() > 0, "Critic checkpoint is empty");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
fn compute_gae_advantages(
|
|
rewards: &[f32],
|
|
values: &[f32],
|
|
dones: &[bool],
|
|
gamma: f32,
|
|
lambda: f32,
|
|
) -> Vec<f32> {
|
|
let n = rewards.len();
|
|
let mut advantages = vec![0.0; n];
|
|
let mut gae = 0.0;
|
|
|
|
for t in (0..n).rev() {
|
|
let reward = rewards[t];
|
|
let value = values[t];
|
|
let next_value = if t + 1 < n { values[t + 1] } else { 0.0 };
|
|
let done = dones[t];
|
|
|
|
let mask = if done { 0.0 } else { 1.0 };
|
|
let delta = reward + gamma * next_value * mask - value;
|
|
gae = delta + gamma * lambda * mask * gae;
|
|
|
|
advantages[t] = gae;
|
|
}
|
|
|
|
advantages
|
|
}
|
|
|
|
fn compute_returns(rewards: &[f32], gamma: f32) -> Vec<f32> {
|
|
let n = rewards.len();
|
|
let mut returns = vec![0.0; n];
|
|
let mut cumulative = 0.0;
|
|
|
|
for t in (0..n).rev() {
|
|
cumulative = rewards[t] + gamma * cumulative;
|
|
returns[t] = cumulative;
|
|
}
|
|
|
|
returns
|
|
}
|