- 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>
427 lines
10 KiB
Markdown
427 lines
10 KiB
Markdown
# 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)
|
|
|
|
```rust
|
|
// 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)
|
|
|
|
```rust
|
|
// Lines 27-28
|
|
pub actor_learning_rate: Option<f64>, // Policy LR
|
|
pub critic_learning_rate: Option<f64>, // Value LR
|
|
```
|
|
|
|
### ✅ Dual Optimizers (ppo/ppo.rs)
|
|
|
|
```rust
|
|
// 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)
|
|
|
|
```bash
|
|
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)
|
|
|
|
```bash
|
|
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)
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
# 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)
|
|
|
|
```yaml
|
|
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)
|
|
|
|
```bash
|
|
# 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)
|
|
|
|
```bash
|
|
# 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)
|
|
|
|
```bash
|
|
./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
|
|
|
|
```bash
|
|
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**:
|
|
```bash
|
|
# 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**:
|
|
```bash
|
|
# 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**:
|
|
```bash
|
|
# 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**:
|
|
```bash
|
|
# Increase both LRs by 2x (maintain ratio)
|
|
--policy-lr 0.000002 --value-lr 0.002
|
|
```
|
|
|
|
---
|
|
|
|
## Code References
|
|
|
|
### train_ppo_parquet.rs (Lines 57-63)
|
|
|
|
```rust
|
|
/// 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)
|
|
|
|
```rust
|
|
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)
|
|
|
|
```rust
|
|
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 ✅
|