Files
foxhunt/PPO_DUAL_LEARNING_RATES_GUIDE.md
jgrusewski 3853988af7 feat(hyperopt): Complete DQN hyperopt analysis and PSO optimizer fix
- 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>
2025-11-02 21:49:07 +01:00

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:

  1. Logs show "Policy learning rate: 0.000001"
  2. Logs show "Value learning rate: 0.001"
  3. Checkpoint files created in /tmp/ppo_test_dual_lr/
  4. 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_rate and critic_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-rate flag (deprecated)
  • Loss stagnation at 1.158-1.159 (Pod 0hczpx9nj1ub88)
  • Comments claimed binary limitation (false alarm)

References

  1. Hyperopt Results: PPO_PARAMETERS_QUICK_REF.md
  2. Deployment Scripts: deploy_ppo_production_corrected.sh
  3. CLAUDE.md: Lines 1-70 (Recent Updates section)
  4. Failed Attempt: Pod 0hczpx9nj1ub88 (2025-11-01)

Last Updated: 2025-11-02 Maintainer: Claude Code (Anthropic) Status: Production Ready