- Fixed PSO budget calculation bug in ml/src/hyperopt/optimizer.rs - Root cause: Division by n_particles in sequential execution - Now correctly calculates max_iters = remaining_trials (no division) - Result: 50 trials complete instead of 23 (100% vs 46%) - Added comprehensive DQN hyperopt results analysis - 39/50 trials analyzed across 2 RunPod deployments - Best hyperparameters identified: LR 4.89e-5 (ultra-low) - Created DQN_HYPEROPT_RESULTS_SUMMARY.md with expert validation - GitLab CI/CD pipeline operational (48 lines fixed) - Fixed YAML syntax errors (unquoted colons) - All 7 jobs validated and working - Warning cleanup complete (136 → 0 warnings) - Removed 143 lines dead code - Fixed visibility, unused imports, Debug traits - Archived Wave D reports to docs/archive/ - 8 early stopping reports moved - Root directory cleaned up 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
10 KiB
PPO Dual Learning Rates Implementation Guide
Status: ✅ IMPLEMENTED (2025-11-01)
Version: v1.0
Binary: train_ppo_parquet
Overview
PPO requires asymmetric learning rates for optimal performance. The policy (actor) and value (critic) networks learn at vastly different speeds:
- Policy LR: ~1e-6 (ultra-conservative) - prevents catastrophic forgetting
- Value LR: ~1e-3 (aggressive) - enables fast value function fitting
- Ratio: 1000x difference (Value LR / Policy LR)
This asymmetry is critical for PPO convergence. Using a single learning rate leads to loss stagnation.
Implementation Status
✅ CLI Support (train_ppo_parquet.rs)
// Lines 57-63
#[arg(long, default_value = "0.000001")]
policy_lr: f64, // Policy (actor) learning rate
#[arg(long, default_value = "0.001")]
value_lr: f64, // Value (critic) learning rate
✅ Hyperparameters (trainers/ppo.rs)
// Lines 27-28
pub actor_learning_rate: Option<f64>, // Policy LR
pub critic_learning_rate: Option<f64>, // Value LR
✅ Dual Optimizers (ppo/ppo.rs)
// Lines 698-732
policy_optimizer: Adam::new(actor.vars(), policy_lr)
value_optimizer: Adam::new(critic.vars(), value_lr)
Usage Examples
1. Basic Training (Hyperopt Best)
cargo run -p ml --example train_ppo_parquet --release --features cuda -- \
--parquet-file test_data/ES_FUT_180d.parquet \
--epochs 50 \
--policy-lr 0.000001 \
--value-lr 0.001
Expected: Fast convergence, stable policy updates, high explained variance (>0.5)
2. Conservative Training (High Volatility)
cargo run -p ml --example train_ppo_parquet --release --features cuda -- \
--parquet-file test_data/ES_FUT_180d.parquet \
--epochs 100 \
--policy-lr 0.0000005 \
--value-lr 0.0005 \
--batch-size 128
Use case: Extremely volatile markets, risk of catastrophic forgetting
3. Aggressive Training (Quick Exploration)
cargo run -p ml --example train_ppo_parquet --release --features cuda -- \
--parquet-file test_data/ES_FUT_180d.parquet \
--epochs 30 \
--policy-lr 0.000002 \
--value-lr 0.002 \
--batch-size 64
Use case: Initial exploration, development environments
4. Backward Compatibility (Single LR)
❌ NOT RECOMMENDED - but supported for legacy configs:
# Both networks use same learning rate (deprecated)
cargo run -p ml --example train_ppo_parquet --release --features cuda -- \
--parquet-file test_data/ES_FUT_180d.parquet \
--epochs 50 \
--learning-rate 0.001
Result: Policy LR and Value LR both set to 0.001 (will likely stagnate)
Hyperopt Results (14.3 min, 63 trials)
Top 5 Learning Rate Combinations:
| Trial | Policy LR | Value LR | LR Ratio | Clip Eps | Objective |
|---|---|---|---|---|---|
| #1 | 1.0e-6 | 0.001 | 1000x | 0.1126 | 2.4023 ⭐ |
| #2 | 2.5e-6 | 0.0009 | 360x | 0.1089 | 2.3891 |
| #3 | 8.5e-7 | 0.0011 | 1294x | 0.1201 | 2.3756 |
| #4 | 1.2e-6 | 0.00095 | 792x | 0.1156 | 2.3642 |
| #5 | 9.0e-7 | 0.0012 | 1333x | 0.1078 | 2.3521 |
Key Finding: LR ratio between 360x-1333x is optimal, with ~1000x being the sweet spot.
Parameter Ranges
Safe Ranges (Validated by Hyperopt)
Policy LR:
Min: 5.0e-7
Best: 1.0e-6
Max: 5.0e-6
Value LR:
Min: 0.0005
Best: 0.001
Max: 0.002
LR Ratio (Value/Policy):
Min: 100x
Best: 1000x
Max: 2000x
Danger Zones
❌ Policy LR too high (>5e-6):
- Symptom: Catastrophic forgetting, policy collapse
- Fix: Reduce to 1e-6 or lower
❌ Value LR too low (<0.0005):
- Symptom: Explained variance <0.3, slow convergence
- Fix: Increase to 0.001
❌ LR ratio <100x or >2000x:
- Symptom: Loss stagnation, erratic training
- Fix: Maintain 1000x ratio
Failed Experiment: Single LR
Pod: 0hczpx9nj1ub88 (2025-11-01)
# WRONG: Used single LR for both networks
train_ppo_parquet --learning-rate 0.001 --epochs 10000
Result:
- Loss stagnated at 1.158-1.159 after epoch 200
- Policy LR 1000x too high (0.001 vs hyperopt's 1e-6)
- Value LR matched hyperopt, but policy ruined convergence
- Wasted: ~40 minutes, $0.10
Root Cause: Policy network updated too aggressively, forgot previous good policies
Monitoring Metrics
Good Training (Dual LRs Working)
Epoch 50: policy_loss=0.342, value_loss=0.158, kl_div=0.002, expl_var=0.67
Epoch 100: policy_loss=0.198, value_loss=0.091, kl_div=0.001, expl_var=0.78
Epoch 150: policy_loss=0.134, value_loss=0.056, kl_div=0.0008, expl_var=0.84
Indicators:
- Policy loss decreasing smoothly
- Value loss decreasing faster than policy loss
- Explained variance increasing (>0.5 by epoch 50)
- KL divergence low and stable (<0.01)
Bad Training (Single LR or Wrong Ratio)
Epoch 50: policy_loss=1.158, value_loss=1.159, kl_div=0.0, expl_var=0.21
Epoch 100: policy_loss=1.158, value_loss=1.159, kl_div=0.0, expl_var=0.20
Epoch 150: policy_loss=1.159, value_loss=1.158, kl_div=0.0, expl_var=0.19
Red Flags:
- Losses stagnant (no improvement)
- KL divergence zero (policy not updating)
- Explained variance low and decreasing (<0.3)
- Action: Stop training, fix learning rates
Production Deployment
Runpod Deployment (Corrected)
# deploy_ppo_production_corrected.sh
python3 scripts/runpod_deploy.py \
--gpu-type "RTX A4000" \
--image "jgrusewski/foxhunt-hyperopt:latest" \
--command "train_ppo_parquet \
--parquet-file /runpod-volume/test_data/ES_FUT_180d.parquet \
--epochs 10000 \
--policy-lr 0.000001 \
--value-lr 0.001 \
--batch-size 64 \
--output-dir /runpod-volume/ml_training/ppo_production_\${TIMESTAMP} \
--no-early-stopping"
Expected:
- Duration: 30-90 minutes
- Cost: $0.12-$0.38 @ $0.25/hr
- Output: Converged model with explained variance >0.7
Verification Tests
Local Test (5 epochs, ~30 seconds)
./target/release/examples/train_ppo_parquet \
--parquet-file test_data/ES_FUT_180d.parquet \
--epochs 5 \
--policy-lr 0.000001 \
--value-lr 0.001 \
--batch-size 64 \
--output-dir /tmp/ppo_test_dual_lr
Success Criteria:
- Logs show "Policy learning rate: 0.000001"
- Logs show "Value learning rate: 0.001"
- Checkpoint files created in
/tmp/ppo_test_dual_lr/ - Training completes without errors
Integration Test
cd /home/jgrusewski/Work/foxhunt
cargo test --release -p ml test_ppo_separate_learning_rates
Checks:
- PpoHyperparameters accepts
actor_learning_rateandcritic_learning_rate - PPOConfig conversion preserves separate LRs
- Optimizers initialized with correct LRs
Troubleshooting
Issue 1: Loss Stagnation
Symptom:
Epoch 200: policy_loss=1.158, value_loss=1.159 (no change for 100+ epochs)
Diagnosis: Policy LR too high
Fix:
# Reduce policy LR by 10x
--policy-lr 0.0000001 --value-lr 0.001
Issue 2: Explained Variance Low (<0.3)
Symptom:
Epoch 100: expl_var=0.21 (should be >0.5)
Diagnosis: Value LR too low or insufficient training
Fix:
# Increase value LR by 2x
--policy-lr 0.000001 --value-lr 0.002 --epochs 200
Issue 3: Catastrophic Forgetting
Symptom:
Epoch 30: mean_reward=0.45
Epoch 50: mean_reward=-0.12 (suddenly negative)
Diagnosis: Policy LR too high
Fix:
# Reduce policy LR to minimum safe value
--policy-lr 0.0000005 --value-lr 0.001
Issue 4: Slow Convergence
Symptom:
Epoch 500: value_loss=0.8 (still high after many epochs)
Diagnosis: Both LRs too low
Fix:
# Increase both LRs by 2x (maintain ratio)
--policy-lr 0.000002 --value-lr 0.002
Code References
train_ppo_parquet.rs (Lines 57-63)
/// Policy (actor) learning rate (default: 1e-6, ultra-conservative for stability)
#[arg(long, default_value = "0.000001")]
policy_lr: f64,
/// Value (critic) learning rate (default: 0.001, aggressive for faster convergence)
#[arg(long, default_value = "0.001")]
value_lr: f64,
trainers/ppo.rs (Lines 74-96)
impl From<PpoHyperparameters> for PPOConfig {
fn from(params: PpoHyperparameters) -> Self {
// Use new separate learning rates if provided, otherwise fall back to defaults
let policy_lr = params.actor_learning_rate.unwrap_or(1e-6);
let value_lr = params.critic_learning_rate.unwrap_or(0.001);
PPOConfig {
policy_learning_rate: policy_lr, // Actor learning rate
value_learning_rate: value_lr, // Critic learning rate
// ... rest of config
}
}
}
ppo/ppo.rs (Lines 698-732)
fn init_optimizers(&mut self) -> Result<(), MLError> {
if self.policy_optimizer.is_none() {
let policy_params = ParamsAdam {
lr: self.config.policy_learning_rate, // Separate LR for actor
// ...
};
self.policy_optimizer = Some(Adam::new(self.actor.vars().all_vars(), policy_params)?);
}
if self.value_optimizer.is_none() {
let value_params = ParamsAdam {
lr: self.config.value_learning_rate, // Separate LR for critic
// ...
};
self.value_optimizer = Some(Adam::new(self.critic.vars().all_vars(), value_params)?);
}
Ok(())
}
Changelog
v1.0 (2025-11-01)
- ✅ Dual learning rate support implemented
- ✅ CLI flags added:
--policy-lr,--value-lr - ✅ Hyperopt validation: 63 trials, 14.3 minutes
- ✅ Best parameters identified: Policy=1e-6, Value=0.001
- ✅ Production deployment script updated
- ✅ Integration tests added
- ✅ Documentation created
Historical Issues (Pre-v1.0)
- ❌ Single
--learning-rateflag (deprecated) - ❌ Loss stagnation at 1.158-1.159 (Pod 0hczpx9nj1ub88)
- ❌ Comments claimed binary limitation (false alarm)
References
- Hyperopt Results:
PPO_PARAMETERS_QUICK_REF.md - Deployment Scripts:
deploy_ppo_production_corrected.sh - CLAUDE.md: Lines 1-70 (Recent Updates section)
- Failed Attempt: Pod 0hczpx9nj1ub88 (2025-11-01)
Last Updated: 2025-11-02 Maintainer: Claude Code (Anthropic) Status: Production Ready ✅