spec(crt-train): output smoothness retraining intervention (intervention A)

Per project_crt_diag_findings.md — CRT.diag + diag.2 empirically
falsified all three hypotheses about why the model's per-event output
doesn't track horizon-specific dynamics. Labels ARE differentiated per
horizon, AUC=0.66 IS per-event measured, but per-event predictions
behave identically across all 5 horizons (2.5-event mean run length
raw, 3.3-event after aggressive Wiener-α EMA smoothing). h6000
predictions should change every thousands of events, not every 3.3.

Diagnosis: training dynamics issue. The BCE loss provides no signal
pushing toward horizon-coherent predictions. Adjacent-event labels at
horizon K share K-1/K of the forward window so SHOULD produce similar
predictions, but the loss doesn't require this.

Intervention A (this spec, minimum-scope): output smoothness
regularizer
  L_smooth[h] = λ[h] × mean_t (p[h](t) - p[h](t-1))²

With horizon-weighted λ (stronger for h6000 than h30): forces h6000
predictions to change slowly while leaving h30 responsive.

Implementation surface:
  - multi_horizon_heads.cu backward: extend grad_probs with smoothness
    gradient
  - perception.rs trainer step: prev_probs_d buffer, per-horizon (p[t]
    - p[t-1])² accumulation
  - heads.rs: SMOOTHNESS_LAMBDA constant per horizon
  - alpha_train.rs: --smoothness-base-lambda CLI flag

Validation gate (CRT.train Gate):
  MUST: per-horizon AUC ≥ baseline - 0.02 (no signal destruction)
  WIN: h6000 mean_run_len ≥ 100 events; h6000/h30 ratio ≥ 10× (horizons
       actually differentiated post-training)
  STRETCH: CRT.1 controller smoke shows mean PnL CORRELATED with
       conviction (vs anti-correlated in lnfwd)

Future work if intervention A doesn't pass WIN:
  B: horizon-conditional output structure (architecture change)
  C: curriculum on horizon (multi-day training)
  D: different label generation (majority-vote vs single-point)

Status: Design — awaiting user review before plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-20 22:59:49 +02:00
parent 0e17fd4f2d
commit faa9a73c10

View File

@@ -0,0 +1,137 @@
# CRT.train — Output Smoothness Retraining Intervention (Design)
**Date:** 2026-05-20
**Branch context:** `ml-alpha-phase-a` (HEAD `0e17fd4f2`; CRT.1 implementation done; CRT.diag + diag.2 empirically falsified the input-side fix)
**Status:** Design — pending user review before plan
**Predicate:** `project_crt_diag_findings.md` (empirical conclusion that retraining is required)
---
## 1. Motivation
The CRT.1 controller architecture is implemented and correct. But the underlying perception model produces per-event outputs that don't track horizon-specific dynamics:
| Empirical finding (lnfwd smoke @ 0e17fd4f2) | Implication |
|---|---|
| All 5 horizons have identical 2.5-event mean run length BEFORE smoothing | Heads collapsed to the same output behavior |
| All 5 horizons have 3.0-3.3 mean run length AFTER aggressive smoothing | Even with smoothing, h6000 doesn't track its supposed horizon (should be thousands) |
| Win rate flat 20-23% across all conviction buckets | Conviction is not predictive |
| mean PnL anti-correlated with conviction (-45 → -84 monotonically worsening) | The "high conviction" prediction is the WRONG direction more often |
The model's per-event output has training-time AUC=0.66, but per-event predictions don't behave as horizon-differentiated multi-step predictors. No controller-side intervention can fix this.
## 2. Hypothesis
The per-event output collapse is a TRAINING DYNAMICS issue, not a data or architecture issue:
- Labels per horizon ARE differentiated (`multi_horizon_labels::generate_labels` produces `sign(price[t+K]-price[t])` per K). Refuted as label-side issue.
- AUC=0.66 IS measured per-event (Mann-Whitney U). Refuted as metric-side issue.
- Architecture HAS 5 separate heads × 128 hidden weights. Heads can in principle differentiate.
What's missing: the loss function provides no signal pushing toward per-horizon-coherent predictions. BCE on per-event labels alone:
1. Doesn't penalize per-event jitter (adjacent labels share K-1/K of the forward window so SHOULD produce similar predictions, but the model isn't required to)
2. Doesn't differentiate horizons structurally (the 5 BCE losses are summed/averaged; nothing in the gradient says "h6000 should change slower than h30")
The minimal-scope intervention: add an **output smoothness regularizer** that penalizes per-event prediction jitter, weighted by horizon (stronger for h6000 than h30).
## 3. Architectural Intervention
### 3.1 Output smoothness regularizer
For each horizon h, add a regularization term to the training loss:
```
L_smooth[h] = λ[h] × mean_t (p[h](t) - p[h](t-1))²
```
Where:
- `p[h](t)` is the model's per-event sigmoid output at event t for horizon h
- `λ[h]` is a horizon-specific weight, **strongly increasing with K**: e.g. `λ[h] = base_λ × (K_h / K_min)`
- For HORIZONS = [30, 100, 300, 1000, 6000]: λ ratios = 1, 3.3, 10, 33, 200 (or some chosen scale)
Composite loss: `L_total = L_BCE_per_horizon + L_smooth_per_horizon`
The intent: h6000 predictions are required to change very slowly (high penalty for fast changes); h30 predictions are allowed to be more responsive.
### 3.2 Why this is the minimum-scope intervention
Other options considered:
- **Architecture change**: horizon-conditional output structure. Larger code change; not necessary if regularizer suffices.
- **Curriculum**: train h6000 first then add shorter horizons. Multi-day training cycle; not needed if regularizer suffices.
- **Different label generation**: majority-vote over K-window instead of single-point K-ahead. Requires label regeneration; not needed if regularizer suffices.
Order of preference: A (this spec) → B (architecture) → C (curriculum) → D (labels), each only if the prior didn't work.
## 4. Implementation Surface
**Files to modify:**
- `crates/ml-alpha/cuda/multi_horizon_heads.cu` — backward kernel: extend the existing `grad_probs` computation to include the smoothness gradient term. Forward pass unchanged (still produces 5 per-event sigmoid probs).
- `crates/ml-alpha/src/trainer/perception.rs` — training step: maintain a per-horizon `prev_probs_d` buffer; after forward, compute `(p[t] - p[t-1])²` per horizon × per batch position; add to total loss with `λ[h]` weighting; backward passes through the smoothness term.
- `crates/ml-alpha/src/heads.rs` — add `pub const SMOOTHNESS_LAMBDA: [f32; N_HORIZONS] = [...]` config constants.
- `crates/ml-alpha/examples/alpha_train.rs` — CLI flag `--smoothness-base-lambda <f32>` (default e.g. 0.01); log per-horizon `(L_BCE, L_smooth)` separately.
**Files NOT to modify:**
- `multi_horizon_labels.rs` — labels unchanged (this is intervention A, not D)
- `heads.rs` forward path — output structure unchanged (intervention A, not B)
- Any inference-time code (`forward_only` / `forward_step` / CRT.1 controller) — they consume whatever the trained model produces; if smoother outputs help, they help.
## 5. Validation Gate (CRT.train Gate)
After retraining a fresh checkpoint with the smoothness regularizer, run CRT.diag smoke with the existing CRT.1 controller (no controller changes). Acceptance criteria:
**MUST (no-regression on signal quality):**
- Per-horizon AUC ≥ baseline_AUC 0.02 (smoothness shouldn't destroy AUC)
- Training loss converges within reasonable epoch count (regularizer doesn't destabilize)
- No NaN propagation
**WIN (signal differentiation by horizon):**
- h6000 mean_run_len ≥ 100 events (vs 3.3 in lnfwd)
- h30 mean_run_len ≤ 10 events (faster, as intended)
- h6000 mean_run_len / h30 mean_run_len ≥ 10× ratio (horizons actually differentiated)
**STRETCH (alpha-realization):**
- CRT.1 controller smoke run shows mean PnL CORRELATED with conviction (not anti-correlated as in lnfwd)
- OR win rate increasing with conviction (vs flat 22%)
If WIN passes but STRETCH doesn't, the model output is now horizon-differentiated but still not predictive of trade outcomes — this would point to interventions B/C/D.
If WIN fails, regularizer wasn't enough — escalate to intervention B (architecture change).
## 6. Risks
| Risk | Severity | Mitigation |
|---|---|---|
| Over-smoothing destroys AUC | High | Per-horizon `λ` tunable; start at small `base_λ` and sweep |
| Smoothness gradient explodes with high λ | Medium | Standard gradient clipping; existing optimizer handles this |
| Training time bloats from extra ops | Low | (p[t] - p[t-1])² is O(B × K × N_HORIZONS) — negligible vs trunk fwd cost |
| The smoothness intervention fixes flip rate but not predictiveness | Medium | This is the "WIN without STRETCH" scenario; spec includes the escalation path to interventions B/C/D |
## 7. Open Questions
1. **What's the right `base_λ`?** Will be a sweep — start with 0.01 / 0.001 / 0.1 and pick the best on holdout AUC vs h6000 mean_run_len trade-off.
2. **Should smoothness be measured per-sequence-position or per-time?** The trainer processes K-windows. Within a window, all 64 positions are present. Smoothness can be: (a) within-window adjacent-position differences, OR (b) cross-window for the last position of consecutive windows. (a) is cheaper and dominant.
3. **Per-batch vs per-fold smoothness target?** If the model has access to `t` and `t-1` predictions in the same batch (sequential window), regularization is natural. If batches are random samples, regularization needs adjacent-event pairs as input. Check loader.rs structure.
4. **Does the existing training pipeline support adjacent-event pairs?** If not, training data structure may need adjustment (sequential vs random batches).
## 8. Out of Scope (deferred to interventions B/C/D if needed)
- Architecture change to horizon-conditional outputs
- Curriculum training (h6000 first)
- Label regeneration (majority-vote vs single-point K-ahead)
- Multi-task losses (e.g., predicting both direction and magnitude)
- Online weight adaptation (was Phase CRT.3 of the controller plan; would shift downstream)
## 9. Predecessor and Successor
**Predecessor:** `project_crt_diag_findings.md` (empirical falsification of the input-side fix)
**Successor (if WIN passes):** retrain with smoothness regularizer, validate via CRT.diag re-run, then resume CRT controller work (CRT.1 retest with new checkpoint, or proceed to CRT.2 envelope work).
**Successor (if WIN fails):** intervention B (architecture change) spec.
---
**End of CRT.train (Intervention A) design. Awaiting user review before implementation plan.**