Files
foxhunt/docs/agent7_missing_getters.md
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

332 lines
8.6 KiB
Markdown

# Agent 7: Missing DQNTrainer Getters Analysis
## Required Getters for Overfitting Detection
To build `ValidationMetrics` history, we need access to per-epoch data from `DQNTrainer`.
---
## Current Status
### ✅ Existing Getters (verified in trainer.rs)
1. **`pub fn get_best_val_loss(&self) -> f64`** (line 3390)
- Returns: Single scalar (best validation loss)
- **Issue**: We need **history** not just best value
2. **`pub fn get_best_epoch(&self) -> usize`** (line 3397)
- Returns: Epoch number with best val loss
- **Issue**: Not a history vector
3. **`pub fn get_val_data(&self) -> &[(FeatureVector51, Vec<f64>)]`** (line 3406)
- Returns: Validation dataset (raw data, not loss history)
- **Issue**: Not loss history
4. **`pub fn get_agent(&self) -> &Arc<RwLock<DQNAgentType>>`** (line 3448)
- Returns: DQN agent reference
- **Usage**: Can query current state but not history
---
## ❌ Missing Getters (need to add)
### 1. Training Loss History
```rust
/// Get training loss per epoch
///
/// Returns vector where `history[i]` is the average training loss
/// for epoch `i` (0-indexed).
///
/// Used by hyperopt adapter to build ValidationMetrics for overfitting detection.
pub fn get_loss_history(&self) -> &[f64] {
&self.loss_history
}
```
**Backing field**: `loss_history: Vec<f64>` (line 337, already exists)
**Verification**: Exists in struct, just needs public getter
---
### 2. Validation Loss History
```rust
/// Get validation loss per epoch
///
/// Returns vector where `history[i]` is the validation loss
/// for epoch `i` (0-indexed).
///
/// Used by hyperopt adapter to detect train/val divergence (overfitting).
pub fn get_val_loss_history(&self) -> &[f64] {
&self.val_loss_history
}
```
**Backing field**: `val_loss_history: Vec<f64>` (line 344, already exists)
**Verification**: Exists in struct, just needs public getter
---
### 3. Q-Value History
```rust
/// Get mean Q-value per epoch
///
/// Returns vector where `history[i]` is the average Q-value
/// across all actions for epoch `i` (0-indexed).
///
/// Used by hyperopt adapter to monitor Q-value stability.
pub fn get_q_value_history(&self) -> &[f64] {
&self.q_value_history
}
```
**Backing field**: `q_value_history: Vec<f64>` (line 338, already exists)
**Verification**: Exists in struct, just needs public getter
---
### 4. Action Distribution
```rust
/// Get final action distribution (BUY%, SELL%, HOLD%)
///
/// Returns the percentage of each action taken during training
/// as of the most recent epoch.
///
/// Used by hyperopt adapter to detect action collapse.
pub fn get_action_distribution(&self) -> (f64, f64, f64) {
// Get from portfolio tracker or compute from recent episode data
self.portfolio_tracker.get_action_distribution()
.unwrap_or((0.33, 0.33, 0.34)) // Default uniform
}
```
**Backing field**: Needs computation from `portfolio_tracker` or new tracking
**Verification**: Need to check if `PortfolioTracker` has this method
---
### 5. Final Gradient Norm
```rust
/// Get most recent gradient norm
///
/// Returns the L2 norm of gradients from the most recent training step.
/// Used to detect gradient explosion.
///
/// Returns `None` if no gradient has been computed yet.
pub fn get_final_gradient_norm(&self) -> Option<f64> {
self.last_gradient_norm
}
```
**Backing field**: Need to add `last_gradient_norm: Option<f64>` to struct
**Verification**: Currently not tracked, needs new field
---
### 6. Q-Value Standard Deviation
```rust
/// Get Q-value standard deviation
///
/// Returns the standard deviation of Q-values across all actions
/// from the most recent epoch. Used to detect Q-value instability.
///
/// Returns `None` if not computed yet.
pub fn get_q_value_std(&self) -> Option<f64> {
self.last_q_value_std
}
```
**Backing field**: Need to add `last_q_value_std: Option<f64>` to struct
**Verification**: Currently not tracked, needs new field
---
## Implementation Plan
### Phase 1: Add Simple History Getters (5 minutes)
Add to `ml/src/trainers/dqn/trainer.rs` (around line 3400):
```rust
/// Get training loss history per epoch
pub fn get_loss_history(&self) -> &[f64] {
&self.loss_history
}
/// Get validation loss history per epoch
pub fn get_val_loss_history(&self) -> &[f64] {
&self.val_loss_history
}
/// Get Q-value history per epoch
pub fn get_q_value_history(&self) -> &[f64] {
&self.q_value_history
}
```
**Risk**: None (just exposing existing private fields)
---
### Phase 2: Add Derived Getters (10 minutes)
```rust
/// Get final action distribution from portfolio tracker
pub fn get_action_distribution(&self) -> (f64, f64, f64) {
// Query portfolio tracker for action counts
let (buy_count, sell_count, hold_count) = self.portfolio_tracker
.get_action_counts();
let total = (buy_count + sell_count + hold_count) as f64;
if total == 0.0 {
return (0.33, 0.33, 0.34); // Default uniform
}
(
buy_count as f64 / total,
sell_count as f64 / total,
hold_count as f64 / total,
)
}
```
**Risk**: Low (depends on `PortfolioTracker` API)
---
### Phase 3: Add Tracking Fields (15 minutes)
**Add to struct** (around line 340):
```rust
pub struct DQNTrainer {
// ... existing fields ...
/// Last computed gradient norm (for gradient explosion detection)
last_gradient_norm: Option<f64>,
/// Last computed Q-value std (for Q-value instability detection)
last_q_value_std: Option<f64>,
}
```
**Update in train loop** (around line 1950):
```rust
// After computing gradients
let grad_norm = compute_gradient_norm(&gradients);
self.last_gradient_norm = Some(grad_norm);
// After computing Q-values
let q_std = compute_q_value_std(&q_values);
self.last_q_value_std = Some(q_std);
```
**Add getters**:
```rust
pub fn get_final_gradient_norm(&self) -> Option<f64> {
self.last_gradient_norm
}
pub fn get_q_value_std(&self) -> Option<f64> {
self.last_q_value_std
}
```
**Risk**: Medium (requires finding where gradients are computed)
---
## Verification Checklist
### Before Implementation
- [ ] Verify `loss_history`, `val_loss_history`, `q_value_history` exist in struct
- [ ] Check if `PortfolioTracker` has `get_action_counts()` or similar
- [ ] Identify where gradients are computed in training loop
- [ ] Identify where Q-value statistics are computed
### After Implementation
- [ ] Run `cargo build --package ml` (should compile)
- [ ] Run `cargo test --package ml` (all tests should pass)
- [ ] Verify getters return expected data types
- [ ] Test with hyperopt integration
---
## Alternative Approach (If Getters Too Complex)
### Use Default Values in Overfitting Detection
Instead of requiring all fields, use defaults where data is unavailable:
```rust
// In hyperopt adapter
let action_dist = trainer.get_action_distribution()
.unwrap_or([0.33, 0.33, 0.34]); // Default uniform
let gradient_norm = trainer.get_final_gradient_norm()
.unwrap_or(1.0) as f32; // Default safe value
let q_value_std = trainer.get_q_value_std()
.unwrap_or(10.0) as f32; // Default safe value
```
**Pros**: Works immediately without DQNTrainer changes
**Cons**: Less accurate overfitting detection (missing some signals)
---
## Recommended Path Forward
### Minimal Viable Product (MVP)
**Only add Phase 1 getters** (3 simple history getters):
- `get_loss_history()`
- `get_val_loss_history()`
- `get_q_value_history()`
**Use defaults for**:
- Action distribution: `[0.33, 0.33, 0.34]` (uniform)
- Gradient norm: Final epoch gradient (from logs)
- Q-value std: Estimated from Q-value mean
**Rationale**:
- Train/val divergence is the **primary** overfitting signal
- Requires only `train_loss` and `val_loss` histories
- Other fields are **supplementary** (nice to have)
- Can be enhanced in Phase 2
### Full Implementation
**Add all getters** (Phase 1-3):
- Most accurate overfitting detection
- Enables all ValidationMetrics features
- More work upfront but cleaner long-term
---
## Summary
### ✅ Easy Wins (5 minutes)
```rust
pub fn get_loss_history(&self) -> &[f64] { &self.loss_history }
pub fn get_val_loss_history(&self) -> &[f64] { &self.val_loss_history }
pub fn get_q_value_history(&self) -> &[f64] { &self.q_value_history }
```
### ⚠️ Moderate Effort (10 minutes)
```rust
pub fn get_action_distribution(&self) -> (f64, f64, f64) { /* compute */ }
```
### 🔧 Requires New Tracking (15 minutes)
```rust
pub fn get_final_gradient_norm(&self) -> Option<f64> { /* new field */ }
pub fn get_q_value_std(&self) -> Option<f64> { /* new field */ }
```
### 💡 Recommended: Start with Easy Wins
Implement Phase 1 only, use defaults for other fields. Can enhance later.
---
**Next Step**: Add 3 simple getters to `trainer.rs` and test overfitting detection.