feat(alpha_train): configurable early-stop metric (default mean_auc)

cnjfl evidence: val_loss and mean_auc disagree.
  val_loss best at e3 (0.5592)
  mean_auc best at e4 (0.7670 — new h300 + h6000 peaks)

For downstream trading, ranking quality (AUC) matters more than
probability calibration (BCE loss). New default is mean_auc-based
early stopping, but val_loss/none remain selectable.

AUC is noisier than loss epoch-to-epoch (1-2pt bounces are common
even when long-horizon AUCs are still drifting up under
auto-horizon-weights), so patience defaults bump from 3 → 5.

CLI:  --early-stop-metric {val_loss|mean_auc|none}   default mean_auc
      --early-stop-patience N                         default 5

Argo template parameters added with matching defaults.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-17 11:48:21 +02:00
parent 894188d34f
commit 26d91a816c
3 changed files with 63 additions and 26 deletions

View File

@@ -82,10 +82,19 @@ struct Cli {
lr_min_factor: f32,
/// Early-stop training after this many consecutive epochs without
/// val_loss improvement. 0 disables early stopping.
#[arg(long, default_value_t = 3)]
/// improvement on `early_stop_metric`. 0 disables early stopping.
#[arg(long, default_value_t = 5)]
early_stop_patience: usize,
/// Which metric drives early stopping: "val_loss" (stable, tracks
/// probability calibration) or "mean_auc" (noisier, tracks the
/// ranking quality used for downstream trading). mean_auc needs
/// higher patience (~5) because it bounces 1-2pt epoch-to-epoch
/// even when the model is still genuinely improving on the long
/// horizons that the auto-horizon-weights down-weight.
#[arg(long, default_value = "mean_auc")]
early_stop_metric: String,
/// Custom per-horizon BCE weights (5 floats, comma-separated). When
/// set, overrides the auto/uniform defaults. Example: "1.0,1.0,0.5,0.1,0.02".
#[arg(long)]
@@ -206,11 +215,20 @@ fn main() -> Result<()> {
let mut final_val_auc = [0.5_f32; N_HORIZONS];
let mut n_val_seqs_consumed = 0usize;
// Validate early-stop metric choice upfront.
let early_stop_metric = cli.early_stop_metric.as_str();
anyhow::ensure!(
matches!(early_stop_metric, "val_loss" | "mean_auc" | "none"),
"--early-stop-metric must be one of: val_loss, mean_auc, none (got `{}`)",
early_stop_metric
);
// Best-checkpoint tracking (by val_loss).
let mut best_val_loss = f32::INFINITY;
let mut best_epoch = 0usize;
let mut best_val_auc = [0.5_f32; N_HORIZONS];
let mut epochs_since_improvement = 0usize;
let mut val_loss_no_improvement = 0usize;
let mut mean_auc_no_improvement = 0usize;
let mut early_stopped = false;
let mut epochs_completed = 0usize;
@@ -370,40 +388,47 @@ fn main() -> Result<()> {
n_val_seqs_consumed = val_loader.yielded();
epochs_completed = epoch + 1;
// Best-checkpoint + early-stop bookkeeping (by val_loss).
if val_avg < best_val_loss {
// Best-checkpoint trackers — independent for each metric.
let val_loss_improved = val_avg < best_val_loss;
if val_loss_improved {
best_val_loss = val_avg;
best_epoch = epoch;
best_val_auc = per_horizon_auc;
epochs_since_improvement = 0;
tracing::info!(
epoch, val_loss = val_avg, "new best val_loss"
);
val_loss_no_improvement = 0;
tracing::info!(epoch, val_loss = val_avg, "new best val_loss");
} else {
epochs_since_improvement += 1;
if cli.early_stop_patience > 0 && epochs_since_improvement >= cli.early_stop_patience {
val_loss_no_improvement += 1;
}
let mean_auc = per_horizon_auc.iter().sum::<f32>() / per_horizon_auc.len() as f32;
let mean_auc_improved = mean_auc > best_mean_auc;
if mean_auc_improved {
best_mean_auc = mean_auc;
best_mean_auc_epoch = epoch;
best_mean_auc_per_horizon = per_horizon_auc;
mean_auc_no_improvement = 0;
tracing::info!(epoch, mean_auc, "new best mean_auc");
} else {
mean_auc_no_improvement += 1;
}
// Early-stop decision based on selected metric.
if cli.early_stop_patience > 0 {
let (counter, metric_label, best_val) = match early_stop_metric {
"val_loss" => (val_loss_no_improvement, "val_loss", best_val_loss),
"mean_auc" => (mean_auc_no_improvement, "mean_auc", best_mean_auc),
_ => (0, "none", 0.0), // disabled
};
if counter >= cli.early_stop_patience && metric_label != "none" {
tracing::warn!(
epoch, best_epoch, best_val_loss,
epoch, metric = metric_label, best = best_val,
patience = cli.early_stop_patience,
"early stopping — val_loss did not improve",
"early stopping",
);
early_stopped = true;
break;
}
}
// Independent best-by-mean-AUC tracker — reported only, does NOT
// gate early stopping (val_loss remains the early-stop signal so
// the two policies stay decoupled).
let mean_auc = per_horizon_auc.iter().sum::<f32>() / per_horizon_auc.len() as f32;
if mean_auc > best_mean_auc {
best_mean_auc = mean_auc;
best_mean_auc_epoch = epoch;
best_mean_auc_per_horizon = per_horizon_auc;
tracing::info!(
epoch, mean_auc, "new best mean_auc"
);
}
}
let summary = AlphaTrainSummary {

View File

@@ -66,6 +66,10 @@ spec:
value: "1"
- name: auto-horizon-weights
value: "false"
- name: early-stop-metric
value: "mean_auc"
- name: early-stop-patience
value: "5"
volumes:
- name: git-ssh-key
@@ -431,6 +435,8 @@ spec:
--n-val-seqs {{workflow.parameters.n-val-seqs}} \
--seed {{workflow.parameters.seed}} \
--batch-size {{workflow.parameters.batch-size}} \
--early-stop-metric {{workflow.parameters.early-stop-metric}} \
--early-stop-patience {{workflow.parameters.early-stop-patience}} \
$EXTRA_FLAGS
echo "=== Training complete ==="

View File

@@ -29,6 +29,8 @@ N_VAL_SEQS=1000
SEED=16962
BATCH_SIZE=1
AUTO_HORIZON_WEIGHTS=false
EARLY_STOP_METRIC=mean_auc
EARLY_STOP_PATIENCE=5
WATCH=false
usage() {
@@ -64,6 +66,8 @@ while [[ $# -gt 0 ]]; do
--seed) SEED="$2"; shift 2 ;;
--batch-size) BATCH_SIZE="$2"; shift 2 ;;
--auto-horizon-weights) AUTO_HORIZON_WEIGHTS=true; shift ;;
--early-stop-metric) EARLY_STOP_METRIC="$2"; shift 2 ;;
--early-stop-patience) EARLY_STOP_PATIENCE="$2"; shift 2 ;;
--watch) WATCH=true; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1"; usage; exit 1 ;;
@@ -129,4 +133,6 @@ argo submit -n foxhunt --from=wftmpl/alpha-perception \
-p seed="$SEED" \
-p batch-size="$BATCH_SIZE" \
-p auto-horizon-weights="$AUTO_HORIZON_WEIGHTS" \
-p early-stop-metric="$EARLY_STOP_METRIC" \
-p early-stop-patience="$EARLY_STOP_PATIENCE" \
$WATCH_FLAG