feat: DQN gems — spectral decoupling, manifold mixup, regime replay, family hyperopt
Phase 1: Remove 5 DQNConfig boolean flags (use_soft_updates, use_iqn, enable_q_value_clipping, use_cvar_action_selection, use_count_bonus). All features now unconditionally active — no dead toggle branches. Phase 2: Add spectral decoupling (L2 on Q-value logits, Pezeshki 2021) and manifold mixup (Beta-sampled distribution interpolation with atomic barrier sync) directly in C51 CUDA loss kernel. Zero CPU involvement. Phase 3: Tag Experience transitions with market regime (ADX/CUSUM GPU classifier kernel). Add regime-biased PER sampling via rejection with IS weight correction. Decay factor controls cross-regime bleeding. Phase 4: 6 family intensity scalars for hyperopt (adversarial, regularization, augmentation, loss shaping, ensemble, causal). Scales 34 generalization params through 6 PSO dimensions instead of 34. Search space: 24D → 30D (families additive, individual params kept). 20 files changed, +563/-69 lines. Full workspace compiles clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@ q_gap_threshold = 0.05
|
||||
[replay_buffer]
|
||||
buffer_size = 50000
|
||||
min_replay_size = 500
|
||||
regime_replay_decay = 0.3
|
||||
|
||||
[early_stopping]
|
||||
enabled = true
|
||||
@@ -66,6 +67,7 @@ cql_alpha = 1.0
|
||||
curiosity_weight = 0.1
|
||||
iqn_lambda = 0.25
|
||||
spectral_norm_sigma_max = 1.5
|
||||
spectral_decoupling_lambda = 0.01
|
||||
gradient_clip_norm = 1.0
|
||||
|
||||
[generalization]
|
||||
@@ -98,6 +100,14 @@ enable_mirror_universe = true
|
||||
position_entropy_weight = 0.01
|
||||
trade_clustering_penalty = 0.05
|
||||
ensemble_disagreement_penalty = 0.3
|
||||
mixup_alpha = 0.2
|
||||
# Family intensity scalars for hyperopt (1.0 = neutral, range [0.0, 2.0])
|
||||
adversarial_intensity = 1.0
|
||||
regularization_intensity = 1.0
|
||||
augmentation_intensity = 1.0
|
||||
loss_shaping_intensity = 1.0
|
||||
ensemble_intensity = 1.0
|
||||
causal_intensity = 1.0
|
||||
|
||||
[reward]
|
||||
loss_aversion = 1.5
|
||||
|
||||
@@ -38,6 +38,7 @@ buffer_size = 500000
|
||||
min_replay_size = 1000
|
||||
per_alpha = 0.6
|
||||
per_beta_start = 0.4
|
||||
regime_replay_decay = 0.3
|
||||
|
||||
[early_stopping]
|
||||
enabled = true
|
||||
@@ -71,6 +72,7 @@ cql_alpha = 1.0
|
||||
curiosity_weight = 0.1
|
||||
iqn_lambda = 0.25
|
||||
spectral_norm_sigma_max = 1.5
|
||||
spectral_decoupling_lambda = 0.01
|
||||
gradient_clip_norm = 1.0
|
||||
|
||||
[generalization]
|
||||
@@ -103,6 +105,14 @@ enable_mirror_universe = true
|
||||
position_entropy_weight = 0.01
|
||||
trade_clustering_penalty = 0.05
|
||||
ensemble_disagreement_penalty = 0.3
|
||||
mixup_alpha = 0.2
|
||||
# Family intensity scalars for hyperopt (1.0 = neutral, range [0.0, 2.0])
|
||||
adversarial_intensity = 1.0
|
||||
regularization_intensity = 1.0
|
||||
augmentation_intensity = 1.0
|
||||
loss_shaping_intensity = 1.0
|
||||
ensemble_intensity = 1.0
|
||||
causal_intensity = 1.0
|
||||
|
||||
[reward]
|
||||
loss_aversion = 1.5
|
||||
|
||||
@@ -37,6 +37,7 @@ q_gap_threshold = 0.05
|
||||
[replay_buffer]
|
||||
buffer_size = 10000
|
||||
min_replay_size = 100
|
||||
regime_replay_decay = 0.3
|
||||
|
||||
[early_stopping]
|
||||
enabled = true
|
||||
@@ -70,6 +71,7 @@ cql_alpha = 1.0
|
||||
curiosity_weight = 0.1
|
||||
iqn_lambda = 0.25
|
||||
spectral_norm_sigma_max = 1.5
|
||||
spectral_decoupling_lambda = 0.01
|
||||
gradient_clip_norm = 1.0
|
||||
|
||||
[generalization]
|
||||
@@ -95,6 +97,14 @@ adversarial_warmup_epochs = 100 # Beyond smoketest duration (don't activate s
|
||||
adversarial_checkpoint_interval = 100
|
||||
pruning_epoch = 100 # Beyond 50-epoch test (don't prune tiny network)
|
||||
pruning_fraction = 0.5 # Less aggressive (50% vs production 70%)
|
||||
mixup_alpha = 0.2
|
||||
# Family intensity scalars for hyperopt (1.0 = neutral, range [0.0, 2.0])
|
||||
adversarial_intensity = 1.0
|
||||
regularization_intensity = 1.0
|
||||
augmentation_intensity = 1.0
|
||||
loss_shaping_intensity = 1.0
|
||||
ensemble_intensity = 1.0
|
||||
causal_intensity = 1.0
|
||||
|
||||
[reward]
|
||||
loss_aversion = 1.5
|
||||
|
||||
@@ -20,11 +20,34 @@ pub struct Experience {
|
||||
pub done: bool,
|
||||
/// Experience timestamp
|
||||
pub timestamp: u64,
|
||||
/// Market regime at transition time: 0=Trending, 1=Ranging, 2=Volatile
|
||||
pub regime: u8,
|
||||
}
|
||||
|
||||
/// Classify market regime from a state feature vector.
|
||||
///
|
||||
/// Uses ADX (index 40) and CUSUM (index 41) to determine the market regime:
|
||||
/// - 0 = Trending (ADX > 0.25)
|
||||
/// - 1 = Ranging (default / low ADX, low CUSUM)
|
||||
/// - 2 = Volatile (|CUSUM| > 0.7)
|
||||
///
|
||||
/// Returns 1 (Ranging) as safe default when the state vector is too short.
|
||||
pub fn classify_regime_from_state(state: &[f32]) -> u8 {
|
||||
let adx = if state.len() > 40 { state[40] } else { 0.0 };
|
||||
let cusum = if state.len() > 41 { state[41] } else { 0.0 };
|
||||
if adx > 0.25 {
|
||||
0 // Trending
|
||||
} else if cusum.abs() > 0.7 {
|
||||
2 // Volatile
|
||||
} else {
|
||||
1 // Ranging
|
||||
}
|
||||
}
|
||||
|
||||
impl Experience {
|
||||
/// Create a new experience
|
||||
pub fn new(state: Vec<f32>, action: u8, reward: f32, next_state: Vec<f32>, done: bool) -> Self {
|
||||
let regime = classify_regime_from_state(&state);
|
||||
Self {
|
||||
state,
|
||||
action,
|
||||
@@ -35,6 +58,7 @@ impl Experience {
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos() as u64,
|
||||
regime,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ impl NStepBuffer {
|
||||
next_state: first.next_state,
|
||||
done: first.done,
|
||||
timestamp: first.timestamp,
|
||||
regime: first.regime,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -139,6 +140,7 @@ impl NStepBuffer {
|
||||
next_state: newest.next_state.clone(),
|
||||
done: newest.done,
|
||||
timestamp: first.timestamp,
|
||||
regime: first.regime,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -181,6 +183,7 @@ impl NStepBuffer {
|
||||
next_state,
|
||||
done,
|
||||
timestamp: first.timestamp,
|
||||
regime: first.regime,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -226,6 +229,7 @@ mod tests {
|
||||
next_state: vec![state_val + 1.0; 32],
|
||||
done,
|
||||
timestamp: chrono::Utc::now().timestamp_nanos_opt().unwrap() as u64,
|
||||
regime: 1, // Ranging default for tests
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -744,6 +744,85 @@ impl PrioritizedReplayBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sample with regime affinity weighting via rejection sampling.
|
||||
///
|
||||
/// Oversamples 3x from standard PER, accepts each candidate with probability
|
||||
/// `decay^|regime_distance|`, and corrects IS weights for the selection bias.
|
||||
/// Falls back to standard PER sampling to fill any shortfall.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `batch_size` - Target number of experiences to return
|
||||
/// * `current_regime` - Active market regime (0=Trending, 1=Ranging, 2=Volatile)
|
||||
/// * `decay` - Per-unit regime distance acceptance decay (0.0=same-regime only, 1.0=unbiased)
|
||||
pub fn sample_regime_biased(
|
||||
&self,
|
||||
batch_size: usize,
|
||||
current_regime: u8,
|
||||
decay: f32,
|
||||
) -> Result<(Vec<Experience>, Vec<f32>, Vec<usize>), MLError> {
|
||||
let size = self.size.load(Ordering::Acquire);
|
||||
if size < batch_size {
|
||||
return Err(MLError::TrainingError(format!(
|
||||
"Not enough experiences for regime-biased sampling: {} < {}",
|
||||
size, batch_size
|
||||
)));
|
||||
}
|
||||
|
||||
// Oversample 3x from standard PER (capped at buffer size)
|
||||
let oversample = (batch_size * 3).min(size);
|
||||
let (candidates, cand_weights, cand_indices) = self.sample(oversample)?;
|
||||
|
||||
let mut rng = self.rng.lock();
|
||||
|
||||
let mut selected_exp = Vec::with_capacity(batch_size);
|
||||
let mut selected_weights = Vec::with_capacity(batch_size);
|
||||
let mut selected_indices = Vec::with_capacity(batch_size);
|
||||
|
||||
for (i, exp) in candidates.into_iter().enumerate() {
|
||||
if selected_exp.len() >= batch_size {
|
||||
break;
|
||||
}
|
||||
|
||||
let regime_dist = (exp.regime as i8 - current_regime as i8).unsigned_abs();
|
||||
let regime_factor = decay.powi(regime_dist as i32);
|
||||
|
||||
if rng.gen::<f32>() < regime_factor {
|
||||
// Correct IS weight for regime selection bias
|
||||
selected_weights.push(cand_weights[i] / regime_factor.max(1e-6));
|
||||
selected_indices.push(cand_indices[i]);
|
||||
selected_exp.push(exp);
|
||||
}
|
||||
}
|
||||
|
||||
// If not enough accepted, fill remainder from standard PER (unbiased)
|
||||
if selected_exp.len() < batch_size {
|
||||
let remaining = batch_size - selected_exp.len();
|
||||
drop(rng); // release lock before calling sample()
|
||||
let (extra_exp, extra_w, extra_idx) = self.sample(remaining)?;
|
||||
for (i, exp) in extra_exp.into_iter().enumerate() {
|
||||
if selected_exp.len() >= batch_size {
|
||||
break;
|
||||
}
|
||||
selected_exp.push(exp);
|
||||
selected_weights.push(extra_w[i]);
|
||||
selected_indices.push(extra_idx[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize weights so max = 1.0
|
||||
let max_w = selected_weights
|
||||
.iter()
|
||||
.cloned()
|
||||
.fold(f32::NEG_INFINITY, f32::max);
|
||||
if max_w > 0.0 {
|
||||
for w in &mut selected_weights {
|
||||
*w /= max_w;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((selected_exp, selected_weights, selected_indices))
|
||||
}
|
||||
|
||||
/// Get current training step
|
||||
pub fn training_step(&self) -> usize {
|
||||
self.training_step.load(Ordering::Acquire)
|
||||
|
||||
@@ -919,7 +919,6 @@ fn evaluate_dqn_fold(
|
||||
(10.0 / (1.0 - gamma) * 1.2).clamp(20.0, 300.0)
|
||||
}) as f32,
|
||||
iqn_num_quantiles: hp_usize(hp, "num_quantiles").unwrap_or(64),
|
||||
use_cvar_action_selection: false,
|
||||
..DQNConfig::default()
|
||||
};
|
||||
|
||||
@@ -1088,7 +1087,6 @@ fn evaluate_dqn_fold_gpu(
|
||||
(10.0 / (1.0 - gamma) * 1.2).clamp(20.0, 300.0)
|
||||
}) as f32,
|
||||
iqn_num_quantiles: hp_usize(hp, "num_quantiles").unwrap_or(64),
|
||||
use_cvar_action_selection: false,
|
||||
..DQNConfig::default()
|
||||
};
|
||||
|
||||
|
||||
@@ -421,7 +421,6 @@ impl DqnBenchmarkRunner {
|
||||
tau: 1.0,
|
||||
tau_final: 1.0, // No annealing for hard updates
|
||||
tau_anneal_steps: 0,
|
||||
use_soft_updates: false,
|
||||
|
||||
// Rainbow DQN warmup period (disabled for benchmarking)
|
||||
warmup_steps: 0, // No warmup for fast benchmarks
|
||||
@@ -450,7 +449,6 @@ impl DqnBenchmarkRunner {
|
||||
noisy_sigma_init: 0.5,
|
||||
|
||||
// BUG #37 FIX: Q-value clipping (prevents step-level explosions)
|
||||
enable_q_value_clipping: true,
|
||||
q_value_clip_min: -500.0,
|
||||
q_value_clip_max: 500.0,
|
||||
|
||||
@@ -459,11 +457,9 @@ impl DqnBenchmarkRunner {
|
||||
gradient_collapse_patience: 5,
|
||||
|
||||
cql_alpha: 1.0,
|
||||
use_iqn: false,
|
||||
iqn_num_quantiles: 64,
|
||||
iqn_kappa: 1.0,
|
||||
iqn_embedding_dim: 64,
|
||||
use_cvar_action_selection: false,
|
||||
cvar_alpha: 0.05,
|
||||
|
||||
minimum_profit_factor: 1.5,
|
||||
|
||||
@@ -206,7 +206,15 @@ extern "C" __global__ void c51_loss_batched(
|
||||
|
||||
/* ── #27 Ensemble disagreement Q-penalty ─────────────────────── */
|
||||
const float* __restrict__ ensemble_std, /* [B] per-sample ensemble std (0=no penalty) */
|
||||
float ensemble_disagreement_weight /* scale reward by 1/(1+weight*std) (0.0=disabled) */
|
||||
float ensemble_disagreement_weight, /* scale reward by 1/(1+weight*std) (0.0=disabled) */
|
||||
|
||||
/* ── Spectral decoupling: L2 penalty on Q-value logit magnitudes ── */
|
||||
float spectral_decoupling_lambda, /* Pezeshki et al. 2021 (0.0=disabled, 0.01=default) */
|
||||
|
||||
/* ── Manifold Mixup: interpolate projected C51 target distributions ── */
|
||||
float mixup_alpha, /* Beta(alpha,alpha) mixing coeff. 0.0=disabled */
|
||||
unsigned int mixup_seed, /* per-batch RNG seed */
|
||||
int* __restrict__ mixup_barrier /* [NUM_BRANCHES] atomic counters for inter-block sync */
|
||||
) {
|
||||
extern __shared__ float shmem_f[];
|
||||
|
||||
@@ -295,6 +303,7 @@ extern "C" __global__ void c51_loss_batched(
|
||||
}
|
||||
|
||||
float total_ce = 0.0f;
|
||||
float total_logit_sq = 0.0f; /* Spectral decoupling: accumulates ||logits||^2 across all branches */
|
||||
|
||||
for (int d = 0; d < NUM_BRANCHES; d++) {
|
||||
int n_d = branch_sizes[d];
|
||||
@@ -322,6 +331,26 @@ extern "C" __global__ void c51_loss_batched(
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
/* ── Spectral decoupling: accumulate sum of squared logits across ALL actions ──
|
||||
* Penalizes overconfident Q-value logit magnitudes (Pezeshki et al. 2021).
|
||||
* Computed on the raw dueling logits (V + A_a - mean(A)) BEFORE softmax.
|
||||
* Iterates over all actions in this branch to get full coverage. */
|
||||
if (spectral_decoupling_lambda > 0.0f) {
|
||||
float local_sq = 0.0f;
|
||||
for (int a = 0; a < n_d; a++) {
|
||||
for (int j = tid; j < num_atoms; j += BLOCK_THREADS) {
|
||||
float a_mean = 0.0f;
|
||||
for (int aa = 0; aa < n_d; aa++)
|
||||
a_mean += shmem_adv[aa * num_atoms + j];
|
||||
a_mean /= (float)n_d;
|
||||
float logit = shmem_val[j] + shmem_adv[a * num_atoms + j] - a_mean;
|
||||
local_sq += logit * logit;
|
||||
}
|
||||
}
|
||||
float branch_sq = block_reduce_sum_f(local_sq, shmem_reduce, tid);
|
||||
total_logit_sq += branch_sq;
|
||||
}
|
||||
|
||||
block_log_softmax_f(shmem_lp, shmem_lp, shmem_reduce, tid, num_atoms);
|
||||
|
||||
/* Copy and save current log-probs */
|
||||
@@ -424,6 +453,69 @@ extern "C" __global__ void c51_loss_batched(
|
||||
save_projected[save_off + j] = bf16(shmem_lp[j]);
|
||||
__syncthreads();
|
||||
|
||||
/* ═══ Manifold Mixup: interpolate projected target distributions ═══
|
||||
* Verma et al. 2019 — mix pairs of C51 target distributions to
|
||||
* smooth the learned manifold and improve generalization.
|
||||
*
|
||||
* Uses save_projected buffer for cross-sample reads with an atomic
|
||||
* inter-block barrier to ensure all samples have written their
|
||||
* projected distributions before any sample reads a partner's.
|
||||
*
|
||||
* Lambda ~ Beta(alpha, alpha) via ratio of Kumaraswamy-like rvs. */
|
||||
if (mixup_alpha > 0.0f) {
|
||||
/* Inter-block barrier: all blocks must finish writing save_projected
|
||||
* for branch d before any block reads a partner's projection.
|
||||
* Pattern: atomicAdd → spin until counter == batch_size. */
|
||||
__threadfence(); /* ensure save_projected writes are globally visible */
|
||||
if (tid == 0) {
|
||||
atomicAdd(&mixup_barrier[d], 1);
|
||||
}
|
||||
__syncthreads();
|
||||
if (tid == 0) {
|
||||
/* Spin until all blocks for this branch have arrived */
|
||||
while (atomicAdd(&mixup_barrier[d], 0) < batch_size) {
|
||||
/* busy-wait — lightweight since all blocks execute in ~lockstep */
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
__threadfence(); /* ensure we see partner's save_projected writes */
|
||||
|
||||
/* Pseudo-random partner selection via multiplicative hash (Knuth) */
|
||||
unsigned int h = (unsigned int)sample_id;
|
||||
h ^= mixup_seed + (unsigned int)d * 2654435761u;
|
||||
h *= 2654435761u;
|
||||
h ^= h >> 16;
|
||||
h *= 0x85ebca6bu;
|
||||
h ^= h >> 13;
|
||||
int partner = (int)(h % (unsigned int)batch_size);
|
||||
|
||||
/* Sample lambda from Beta(alpha, alpha) approximation:
|
||||
* lambda = u1^(1/a) / (u1^(1/a) + u2^(1/a)) where u1,u2 ~ Uniform(0,1)
|
||||
* For symmetric Beta(a,a), this yields the correct distribution. */
|
||||
unsigned int rng_state = mixup_seed ^ ((unsigned int)sample_id * 1103515245u + 12345u);
|
||||
rng_state = rng_state * 1664525u + 1013904223u;
|
||||
float u1 = (float)(rng_state & 0x7FFFFFu) / (float)0x7FFFFFu;
|
||||
u1 = fmaxf(u1, 1e-7f);
|
||||
rng_state = rng_state * 1664525u + 1013904223u;
|
||||
float u2 = (float)(rng_state & 0x7FFFFFu) / (float)0x7FFFFFu;
|
||||
u2 = fmaxf(u2, 1e-7f);
|
||||
float inv_alpha = 1.0f / mixup_alpha;
|
||||
float v1 = powf(u1, inv_alpha);
|
||||
float v2 = powf(u2, inv_alpha);
|
||||
float lambda = v1 / (v1 + v2 + 1e-7f);
|
||||
|
||||
/* Read partner's projected distribution from save_projected (BF16 → float)
|
||||
* and mix: proj_mixed = lambda * proj_self + (1-lambda) * proj_partner */
|
||||
long long partner_save_off = ((long long)partner * NUM_BRANCHES + d) * num_atoms;
|
||||
float one_minus_lambda = 1.0f - lambda;
|
||||
for (int j = tid; j < num_atoms; j += BLOCK_THREADS) {
|
||||
float my_proj = shmem_lp[j];
|
||||
float partner_proj = __bfloat162float(save_projected[partner_save_off + j]);
|
||||
shmem_lp[j] = lambda * my_proj + one_minus_lambda * partner_proj;
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
/* ═══ STEP e: Cross-entropy loss ═════════════════════════ */
|
||||
|
||||
float local_ce = 0.0f;
|
||||
@@ -437,6 +529,18 @@ extern "C" __global__ void c51_loss_batched(
|
||||
|
||||
float avg_ce = total_ce / (float)NUM_BRANCHES;
|
||||
|
||||
/* ── Spectral decoupling: add L2 logit magnitude penalty ──────────────
|
||||
* SD_penalty = lambda * ||logits||^2 / num_logits_total
|
||||
* This penalizes the raw Q-value logits (before softmax) across all
|
||||
* branches and all actions, preventing overconfident predictions without
|
||||
* affecting gradient flow through the weight matrices.
|
||||
* Reference: Pezeshki et al. 2021, "Gradient Starvation". */
|
||||
if (spectral_decoupling_lambda > 0.0f) {
|
||||
int total_logits_count = (b0_size + b1_size + b2_size) * num_atoms;
|
||||
float sd_penalty = spectral_decoupling_lambda * total_logit_sq / (float)total_logits_count;
|
||||
avg_ce += sd_penalty;
|
||||
}
|
||||
|
||||
/* #18 Asymmetric DD loss: penalize overconfident Q-values during drawdown.
|
||||
* For C51, overconfidence = high cross-entropy (distribution mismatch).
|
||||
* Scale CE up when in drawdown — makes the loss surface steeper for
|
||||
|
||||
@@ -1892,3 +1892,47 @@ extern "C" __global__ void expert_action_override(
|
||||
int a2 = current % b2_size;
|
||||
out_actions[i] = expert_a0 * b1_size * b2_size + a1 * b2_size + a2;
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
/* Kernel: classify_regime_gpu */
|
||||
/* ================================================================== */
|
||||
|
||||
/**
|
||||
* Classify market regime from state features on GPU.
|
||||
*
|
||||
* ADX at index `adx_idx` (typically 40), CUSUM at index `cusum_idx`
|
||||
* (typically 41) of the state vector. Classification logic mirrors
|
||||
* the CPU-side classify_regime_from_state() in experience.rs:
|
||||
*
|
||||
* ADX > adx_threshold => 0 (Trending)
|
||||
* |CUSUM| > cusum_threshold => 2 (Volatile)
|
||||
* else => 1 (Ranging)
|
||||
*
|
||||
* Grid: ceil(batch_size / 256), Block: 256. One thread per sample.
|
||||
*/
|
||||
extern "C" __global__ void classify_regime_gpu(
|
||||
const float* __restrict__ states, /* [batch_size, state_dim] */
|
||||
unsigned char* __restrict__ regimes, /* [batch_size] output */
|
||||
int batch_size,
|
||||
int state_dim,
|
||||
int adx_idx, /* typically 40 */
|
||||
int cusum_idx, /* typically 41 */
|
||||
float adx_threshold, /* typically 0.25 */
|
||||
float cusum_threshold /* typically 0.7 */
|
||||
) {
|
||||
int idx = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (idx >= batch_size) return;
|
||||
|
||||
float adx = (adx_idx < state_dim) ? states[idx * state_dim + adx_idx] : 0.0f;
|
||||
float cusum = (cusum_idx < state_dim) ? states[idx * state_dim + cusum_idx] : 0.0f;
|
||||
|
||||
unsigned char regime;
|
||||
if (adx > adx_threshold) {
|
||||
regime = 0; /* Trending */
|
||||
} else if (fabsf(cusum) > cusum_threshold) {
|
||||
regime = 2; /* Volatile */
|
||||
} else {
|
||||
regime = 1; /* Ranging */
|
||||
}
|
||||
regimes[idx] = regime;
|
||||
}
|
||||
|
||||
@@ -157,6 +157,9 @@ pub struct GpuDqnTrainConfig {
|
||||
/// Default 3.0 (permits natural Xavier-init scaling, prevents explosion).
|
||||
/// Lower values = tighter constraint = more Q-value stability but less capacity.
|
||||
pub spectral_norm_sigma_max: f32,
|
||||
/// Spectral decoupling: L2 penalty on Q-value logit magnitudes.
|
||||
/// Pezeshki et al. 2021. Default: 0.01.
|
||||
pub spectral_decoupling_lambda: f32,
|
||||
/// N-step returns (default: 1). When > 1, the experience collector pre-computes
|
||||
/// R_n = sum(gamma^i * r_{t+i}) and the C51 Bellman uses gamma^n.
|
||||
pub n_steps: usize,
|
||||
@@ -189,6 +192,10 @@ pub struct GpuDqnTrainConfig {
|
||||
pub ensemble_disagreement_penalty: f32,
|
||||
/// #21 Stochastic depth drop probability. 0.0 = disabled, 0.2 = 20% drop per layer.
|
||||
pub stochastic_depth_prob: f32,
|
||||
/// Manifold Mixup alpha for Beta distribution.
|
||||
/// Mixes pairs of C51 target distributions during training.
|
||||
/// 0.0 = disabled. Range: [0.0, 2.0]. Default: 0.2.
|
||||
pub mixup_alpha: f32,
|
||||
/// #34 Enable causal intervention training.
|
||||
pub enable_causal_intervention: bool,
|
||||
/// #34 Causal regularization weight.
|
||||
@@ -239,6 +246,7 @@ impl Default for GpuDqnTrainConfig {
|
||||
weight_decay: 1e-4, // Standard L2 regularization strength
|
||||
max_grad_norm: 10.0,
|
||||
spectral_norm_sigma_max: 3.0,
|
||||
spectral_decoupling_lambda: 0.01,
|
||||
iqn_lambda: 0.25,
|
||||
iqn_num_quantiles: 0,
|
||||
iqn_embedding_dim: 64,
|
||||
@@ -250,6 +258,7 @@ impl Default for GpuDqnTrainConfig {
|
||||
asymmetric_dd_weight: 0.0,
|
||||
ensemble_disagreement_penalty: 0.0,
|
||||
stochastic_depth_prob: 0.0,
|
||||
mixup_alpha: 0.2,
|
||||
pruning_epoch: 50,
|
||||
pruning_fraction: 0.7,
|
||||
enable_causal_intervention: true, // always on
|
||||
@@ -682,6 +691,10 @@ pub struct GpuDqnTrainer {
|
||||
/// #21 Stochastic depth drop probability (0.0=disabled, 0.2=20% drop).
|
||||
stochastic_depth_prob: f32,
|
||||
|
||||
/// Manifold Mixup: atomic barrier counters [NUM_BRANCHES] for inter-block sync.
|
||||
/// Zeroed before each C51 loss launch. Only used when mixup_alpha > 0.
|
||||
mixup_barrier_buf: CudaSlice<i32>,
|
||||
|
||||
/// #27 Per-sample ensemble std for disagreement Q-penalty.
|
||||
/// Populated by `upload_ensemble_std()` before loss kernel launch.
|
||||
/// Zero-filled when ensemble is disabled.
|
||||
@@ -2422,6 +2435,10 @@ impl GpuDqnTrainer {
|
||||
let sd_seed = (std::process::id() as u32).wrapping_mul(2654435761);
|
||||
stream.memcpy_htod(&[sd_seed], &mut stochastic_depth_rng_state)
|
||||
.map_err(|e| MLError::ModelError(format!("seed sd_rng: {e}")))?;
|
||||
// Manifold Mixup: atomic barrier counters [NUM_BRANCHES=3] for inter-block sync
|
||||
let mixup_barrier_buf = stream.alloc_zeros::<i32>(3)
|
||||
.map_err(|e| MLError::ModelError(format!("alloc mixup_barrier: {e}")))?;
|
||||
|
||||
let curiosity_inference_func = {
|
||||
static CURIOSITY_INFERENCE_CUBIN: &[u8] = include_bytes!(
|
||||
concat!(env!("OUT_DIR"), "/curiosity_inference_kernel.cubin")
|
||||
@@ -2692,6 +2709,7 @@ impl GpuDqnTrainer {
|
||||
stochastic_depth_rng_kernel,
|
||||
stochastic_depth_rng_state,
|
||||
stochastic_depth_prob: sd_prob,
|
||||
mixup_barrier_buf,
|
||||
ensemble_std_buf,
|
||||
ensemble_disagreement_weight: ens_weight,
|
||||
curiosity_inference_func,
|
||||
@@ -3871,6 +3889,9 @@ impl GpuDqnTrainer {
|
||||
self.launch_mse_grad_to_scratch()?;
|
||||
|
||||
// C51 path → main buffers (already zeroed above)
|
||||
// Zero Manifold Mixup inter-block barrier counters [NUM_BRANCHES=3] before C51 launch.
|
||||
self.stream.memset_zeros(&mut self.mixup_barrier_buf)
|
||||
.map_err(|e| MLError::ModelError(format!("zero mixup_barrier: {e}")))?;
|
||||
self.launch_c51_loss()?;
|
||||
self.launch_c51_grad()?;
|
||||
|
||||
@@ -4412,6 +4433,9 @@ impl GpuDqnTrainer {
|
||||
let shmem_floats = na + na + max_branch * na + na + na + na + 8;
|
||||
let shmem_bytes = (shmem_floats * std::mem::size_of::<f32>()) as u32;
|
||||
|
||||
// Manifold Mixup: per-batch seed derived from adam_step (deterministic, no rand dep)
|
||||
let mixup_seed: u32 = (self.adam_step as u32).wrapping_mul(0x9E3779B9);
|
||||
|
||||
unsafe {
|
||||
self.stream
|
||||
.launch_builder(kernel)
|
||||
@@ -4460,6 +4484,12 @@ impl GpuDqnTrainer {
|
||||
// ── #27 Ensemble disagreement (2) ──
|
||||
.arg(&self.ensemble_std_buf)
|
||||
.arg(&self.ensemble_disagreement_weight)
|
||||
// ── Spectral decoupling (1) ──
|
||||
.arg(&self.config.spectral_decoupling_lambda)
|
||||
// ── Manifold Mixup (3): alpha, per-batch RNG seed, atomic barrier ──
|
||||
.arg(&self.config.mixup_alpha)
|
||||
.arg(&mixup_seed)
|
||||
.arg(&self.mixup_barrier_buf)
|
||||
.launch(LaunchConfig {
|
||||
grid_dim: (b as u32, 1, 1),
|
||||
block_dim: (256, 1, 1),
|
||||
@@ -4617,6 +4647,8 @@ impl GpuDqnTrainer {
|
||||
// ── #27 Ensemble disagreement (2) ──
|
||||
.arg(&self.ensemble_std_buf)
|
||||
.arg(&self.ensemble_disagreement_weight)
|
||||
// ── Spectral decoupling (1) ──
|
||||
.arg(&self.config.spectral_decoupling_lambda)
|
||||
.launch(LaunchConfig {
|
||||
grid_dim: (b as u32, 1, 1),
|
||||
block_dim: (256, 1, 1),
|
||||
|
||||
@@ -159,7 +159,10 @@ extern "C" __global__ void mse_loss_batched(
|
||||
|
||||
/* ── #27 Ensemble disagreement Q-penalty ─────────────────────── */
|
||||
const float* __restrict__ ensemble_std, /* [B] per-sample ensemble std (0=no penalty) */
|
||||
float ensemble_disagreement_weight /* Q_target -= weight * std (0.0=disabled) */
|
||||
float ensemble_disagreement_weight, /* Q_target -= weight * std (0.0=disabled) */
|
||||
|
||||
/* ── Spectral decoupling: L2 penalty on Q-value logit magnitudes ── */
|
||||
float spectral_decoupling_lambda /* Pezeshki et al. 2021 (0.0=disabled, 0.01=default) */
|
||||
) {
|
||||
/* Shared memory is now float (4 bytes/elem) — doubled from BF16 version */
|
||||
extern __shared__ float shmem_f[];
|
||||
@@ -244,6 +247,7 @@ extern "C" __global__ void mse_loss_batched(
|
||||
|
||||
float total_mse = 0.0f;
|
||||
float total_abs_td = 0.0f;
|
||||
float total_logit_sq = 0.0f; /* Spectral decoupling: accumulates ||logits||^2 across all branches */
|
||||
|
||||
for (int d = 0; d < NUM_BRANCHES; d++) {
|
||||
int n_d = branch_sizes[d];
|
||||
@@ -274,6 +278,26 @@ extern "C" __global__ void mse_loss_batched(
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
/* ── Spectral decoupling: accumulate sum of squared logits across ALL actions ──
|
||||
* Penalizes overconfident Q-value logit magnitudes (Pezeshki et al. 2021).
|
||||
* Computed on the raw dueling logits (V + A_a - mean(A)) BEFORE softmax.
|
||||
* Iterates over all actions in this branch to get full coverage. */
|
||||
if (spectral_decoupling_lambda > 0.0f) {
|
||||
float local_sq = 0.0f;
|
||||
for (int a = 0; a < n_d; a++) {
|
||||
for (int j = tid; j < num_atoms; j += BLOCK_THREADS) {
|
||||
float a_mean = 0.0f;
|
||||
for (int aa = 0; aa < n_d; aa++)
|
||||
a_mean += shmem_adv[aa * num_atoms + j];
|
||||
a_mean /= (float)n_d;
|
||||
float logit = shmem_val[j] + shmem_adv[a * num_atoms + j] - a_mean;
|
||||
local_sq += logit * logit;
|
||||
}
|
||||
}
|
||||
float branch_sq = block_reduce_sum_f(local_sq, shmem_reduce, tid);
|
||||
total_logit_sq += branch_sq;
|
||||
}
|
||||
|
||||
/* softmax + E[Q], saving probs to save_current_lp (BF16 output) */
|
||||
float online_eq = block_softmax_expected_q_f(
|
||||
shmem_lp, shmem_support, shmem_reduce,
|
||||
@@ -388,6 +412,18 @@ extern "C" __global__ void mse_loss_batched(
|
||||
float avg_mse = total_mse / (float)NUM_BRANCHES;
|
||||
float avg_td = total_abs_td / (float)NUM_BRANCHES;
|
||||
|
||||
/* ── Spectral decoupling: add L2 logit magnitude penalty ──────────────
|
||||
* SD_penalty = lambda * ||logits||^2 / num_logits_total
|
||||
* This penalizes the raw Q-value logits (before softmax) across all
|
||||
* branches and all actions, preventing overconfident predictions without
|
||||
* affecting gradient flow through the weight matrices.
|
||||
* Reference: Pezeshki et al. 2021, "Gradient Starvation". */
|
||||
if (spectral_decoupling_lambda > 0.0f) {
|
||||
int total_logits_count = (b0_size + b1_size + b2_size) * num_atoms;
|
||||
float sd_penalty = spectral_decoupling_lambda * total_logit_sq / (float)total_logits_count;
|
||||
avg_mse += sd_penalty;
|
||||
}
|
||||
|
||||
if (tid == 0) {
|
||||
float weighted_loss = avg_mse * is_weight;
|
||||
per_sample_loss[sample_id] = bf16(weighted_loss);
|
||||
|
||||
@@ -322,6 +322,18 @@ pub struct DQNParams {
|
||||
/// Lower = tighter constraint (more stability, less capacity).
|
||||
/// Default 3.0 (permits Xavier-init scaling, prevents Q-explosion).
|
||||
pub spectral_norm_sigma_max: f64,
|
||||
/// Spectral decoupling: L2 penalty on Q-value logit magnitudes. Default: 0.01.
|
||||
pub spectral_decoupling_lambda: f64,
|
||||
|
||||
/// Regime-aware replay decay factor. PER priorities for cross-regime
|
||||
/// transitions are multiplied by decay^|regime_distance|.
|
||||
/// 1.0 = no bias. 0.0 = only current regime. Default: 0.3.
|
||||
pub regime_replay_decay: f64,
|
||||
|
||||
/// Manifold Mixup alpha for Beta distribution.
|
||||
/// Mixes pairs of C51 target distributions during training (Verma et al. 2019).
|
||||
/// 0.0 = disabled. Range: [0.0, 2.0]. Default: 0.2.
|
||||
pub mixup_alpha: f64,
|
||||
|
||||
/// Hidden dimension base for dynamic network sizing.
|
||||
/// Network shape: [base, base/2, base/4]. Range: [256, 4096], step=256.
|
||||
@@ -394,9 +406,6 @@ pub struct DQNParams {
|
||||
/// Encourages exploration of novel state-action pairs via prediction error.
|
||||
pub curiosity_weight_tunable: f64,
|
||||
|
||||
/// Enable CVaR (Conditional Value-at-Risk) action selection (0.0 = off, 1.0 = on)
|
||||
/// Stored as f64 for hyperopt, rounded to bool in build_hyperparams.
|
||||
pub use_cvar_action_selection: f64,
|
||||
|
||||
/// CVaR alpha confidence level (0.01-0.20). Lower = more risk-averse.
|
||||
/// 0.05 = optimize for worst 5% outcomes.
|
||||
@@ -405,6 +414,20 @@ pub struct DQNParams {
|
||||
/// Minimum bars to hold a position before allowing exit or reversal (integer, [2, 20]).
|
||||
/// Lets PSO discover optimal hold duration for ES futures regime characteristics.
|
||||
pub min_hold_bars: usize,
|
||||
|
||||
// ── Family intensity scalars (6D hyperopt expansion) ──
|
||||
/// Adversarial family intensity: scales saboteur perturbation, warmup, interval. [0.0, 2.0]
|
||||
pub adversarial_intensity: f64,
|
||||
/// Regularization family intensity: scales dropout, spectral norm, stochastic depth, spectral decoupling. [0.0, 2.0]
|
||||
pub regularization_intensity: f64,
|
||||
/// Data augmentation family intensity: scales feature masking, feature noise. [0.0, 2.0]
|
||||
pub augmentation_intensity: f64,
|
||||
/// Loss shaping family intensity: scales asymmetric DD, regret blend, trade clustering, position entropy. [0.0, 2.0]
|
||||
pub loss_shaping_intensity: f64,
|
||||
/// Ensemble family intensity: scales ensemble disagreement penalty. [0.0, 2.0]
|
||||
pub ensemble_intensity: f64,
|
||||
/// Causal family intensity: scales causal weight, intervention interval. [0.0, 2.0]
|
||||
pub causal_intensity: f64,
|
||||
}
|
||||
|
||||
impl Default for DQNParams {
|
||||
@@ -473,6 +496,9 @@ impl Default for DQNParams {
|
||||
qr_kappa: 1.0,
|
||||
iqn_lambda: 0.25, // Default: mild IQN regularization alongside C51
|
||||
spectral_norm_sigma_max: 3.0, // Default: permits Xavier scaling
|
||||
spectral_decoupling_lambda: 0.01, // Default: mild logit magnitude penalty (Pezeshki 2021)
|
||||
regime_replay_decay: 0.3, // Default: moderate cross-regime decay
|
||||
mixup_alpha: 0.2, // Manifold Mixup: Beta(0.2,0.2) target distribution mixing
|
||||
hidden_dim_base: 256, // Conservative default (backward compatible)
|
||||
noisy_epsilon_floor: 0.10, // Minimum 10% random exploration to prevent action collapse
|
||||
count_bonus_coefficient: 0.05, // UCB directed exploration — complementary to NoisyNet
|
||||
@@ -495,9 +521,15 @@ impl Default for DQNParams {
|
||||
// Task 11: New hyperopt-tunable parameters
|
||||
her_ratio: 0.2, // Default: 20% HER relabeling
|
||||
curiosity_weight_tunable: 0.1, // Default: 10% curiosity-driven intrinsic reward
|
||||
use_cvar_action_selection: 1.0, // Default: enabled (risk-aware IQN)
|
||||
cvar_alpha: 0.05, // Default: worst 5% tail
|
||||
min_hold_bars: 5, // Default: 5 bars minimum hold
|
||||
// Family intensity scalars: 1.0 = neutral (no change to defaults)
|
||||
adversarial_intensity: 1.0,
|
||||
regularization_intensity: 1.0,
|
||||
augmentation_intensity: 1.0,
|
||||
loss_shaping_intensity: 1.0,
|
||||
ensemble_intensity: 1.0,
|
||||
causal_intensity: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -559,6 +591,12 @@ impl ParameterSpace for DQNParams {
|
||||
iq, // 21: iqn_lambda
|
||||
wdd, // 22: w_dd (drawdown penalty weight)
|
||||
ddt, // 23: dd_threshold (drawdown start)
|
||||
(0.0, 2.0), // 24: adversarial_intensity
|
||||
(0.0, 2.0), // 25: regularization_intensity
|
||||
(0.0, 2.0), // 26: augmentation_intensity
|
||||
(0.0, 2.0), // 27: loss_shaping_intensity
|
||||
(0.0, 2.0), // 28: ensemble_intensity
|
||||
(0.0, 2.0), // 29: causal_intensity
|
||||
];
|
||||
|
||||
// Phase Fast: fix architecture dims to single-point bounds
|
||||
@@ -691,19 +729,28 @@ impl ParameterSpace for DQNParams {
|
||||
gradient_accumulation_steps: 1,
|
||||
dt_pretrain_epochs: 0,
|
||||
spectral_norm_sigma_max: 5.0,
|
||||
spectral_decoupling_lambda: 0.01,
|
||||
regime_replay_decay: 0.3,
|
||||
mixup_alpha: 0.2,
|
||||
num_quantiles: 32,
|
||||
qr_kappa: 1.0,
|
||||
her_ratio: 0.3,
|
||||
curiosity_weight_tunable: 0.1,
|
||||
use_cvar_action_selection: 0.0,
|
||||
cvar_alpha: 0.05,
|
||||
// Family intensity scalars (indices 24-29, linear [0.0, 2.0])
|
||||
adversarial_intensity: if x.len() > 24 { x[24].clamp(0.0, 2.0) } else { 1.0 },
|
||||
regularization_intensity: if x.len() > 25 { x[25].clamp(0.0, 2.0) } else { 1.0 },
|
||||
augmentation_intensity: if x.len() > 26 { x[26].clamp(0.0, 2.0) } else { 1.0 },
|
||||
loss_shaping_intensity: if x.len() > 27 { x[27].clamp(0.0, 2.0) } else { 1.0 },
|
||||
ensemble_intensity: if x.len() > 28 { x[28].clamp(0.0, 2.0) } else { 1.0 },
|
||||
causal_intensity: if x.len() > 29 { x[29].clamp(0.0, 2.0) } else { 1.0 },
|
||||
};
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
fn to_continuous(&self) -> Vec<f64> {
|
||||
// 24D search space (consolidated from 46D)
|
||||
// 30D search space (24 original + 6 family intensities)
|
||||
vec![
|
||||
self.learning_rate.ln(), // 0: learning_rate (log)
|
||||
self.batch_size as f64, // 1: batch_size
|
||||
@@ -729,11 +776,17 @@ impl ParameterSpace for DQNParams {
|
||||
self.iqn_lambda, // 21: iqn_lambda
|
||||
self.w_dd, // 22: w_dd
|
||||
self.dd_threshold, // 23: dd_threshold
|
||||
self.adversarial_intensity, // 24: adversarial_intensity
|
||||
self.regularization_intensity, // 25: regularization_intensity
|
||||
self.augmentation_intensity, // 26: augmentation_intensity
|
||||
self.loss_shaping_intensity, // 27: loss_shaping_intensity
|
||||
self.ensemble_intensity, // 28: ensemble_intensity
|
||||
self.causal_intensity, // 29: causal_intensity
|
||||
]
|
||||
}
|
||||
|
||||
fn param_names() -> Vec<&'static str> {
|
||||
// 24 tuned parameters
|
||||
// 30 tuned parameters (24 original + 6 family intensities)
|
||||
vec![
|
||||
"learning_rate", // 0
|
||||
"batch_size", // 1
|
||||
@@ -759,6 +812,12 @@ impl ParameterSpace for DQNParams {
|
||||
"iqn_lambda", // 21
|
||||
"w_dd", // 22
|
||||
"dd_threshold", // 23
|
||||
"adversarial_intensity", // 24
|
||||
"regularization_intensity", // 25
|
||||
"augmentation_intensity", // 26
|
||||
"loss_shaping_intensity", // 27
|
||||
"ensemble_intensity", // 28
|
||||
"causal_intensity", // 29
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2504,7 +2563,7 @@ impl HyperparameterOptimizable for DQNTrainer {
|
||||
};
|
||||
|
||||
// Create DQN hyperparameters from optimization params
|
||||
let hyperparams = DQNHyperparameters {
|
||||
let mut hyperparams = DQNHyperparameters {
|
||||
learning_rate: params.learning_rate,
|
||||
batch_size: params.batch_size,
|
||||
gamma: params.gamma,
|
||||
@@ -2588,6 +2647,7 @@ impl HyperparameterOptimizable for DQNTrainer {
|
||||
// P0 CRITICAL: Prioritized Experience Replay (now tunable in hyperopt)
|
||||
per_alpha: params.per_alpha, // Tunable 0.4-0.8 (default: 0.6)
|
||||
per_beta_start: params.per_beta_start, // Tunable 0.2-0.6 (default: 0.4)
|
||||
regime_replay_decay: params.regime_replay_decay, // Regime-aware replay bias (default: 0.3)
|
||||
|
||||
// Wave 2.1: Dueling Networks
|
||||
dueling_hidden_dim: params.dueling_hidden_dim, // Wave 6.3: Use hyperopt value
|
||||
@@ -2693,6 +2753,8 @@ impl HyperparameterOptimizable for DQNTrainer {
|
||||
qr_kappa: params.qr_kappa,
|
||||
iqn_lambda: params.iqn_lambda,
|
||||
spectral_norm_sigma_max: params.spectral_norm_sigma_max,
|
||||
spectral_decoupling_lambda: params.spectral_decoupling_lambda,
|
||||
mixup_alpha: params.mixup_alpha,
|
||||
|
||||
// Conservative Q-Learning (CQL) — wired from hyperopt search space
|
||||
cql_alpha: params.cql_alpha,
|
||||
@@ -2769,13 +2831,23 @@ impl HyperparameterOptimizable for DQNTrainer {
|
||||
// Decision Transformer pre-training: disabled for all hyperopt trials
|
||||
dt_pretrain_epochs: params.dt_pretrain_epochs,
|
||||
// Task 11: CVaR action selection — wired from hyperopt search space (indices 43-44)
|
||||
use_cvar_action_selection: params.use_cvar_action_selection.round() >= 1.0,
|
||||
cvar_alpha: params.cvar_alpha as f32,
|
||||
// Task 4: min_hold_bars wired from hyperopt search space (index 45)
|
||||
min_hold_bars: params.min_hold_bars,
|
||||
// Family intensity scalars (indices 24-29 in 30D search space)
|
||||
adversarial_intensity: params.adversarial_intensity,
|
||||
regularization_intensity: params.regularization_intensity,
|
||||
augmentation_intensity: params.augmentation_intensity,
|
||||
loss_shaping_intensity: params.loss_shaping_intensity,
|
||||
ensemble_intensity: params.ensemble_intensity,
|
||||
causal_intensity: params.causal_intensity,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Apply family intensity scaling AFTER construction, BEFORE training.
|
||||
// This multiplies the per-parameter defaults by the family scalar (1.0 = no change).
|
||||
hyperparams.apply_family_scaling();
|
||||
|
||||
let data_path_str = self
|
||||
.dbn_data_dir.to_str()
|
||||
.ok_or_else(|| MLError::ConfigError("Invalid UTF-8 in data path".to_owned()))?;
|
||||
|
||||
@@ -194,13 +194,7 @@ impl DQNAgentType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether count-based exploration bonus is enabled.
|
||||
pub fn use_count_bonus(&self) -> bool {
|
||||
match self {
|
||||
Self::Standard(agent) => agent.config.use_count_bonus,
|
||||
Self::RegimeConditional(agent) => agent.config().use_count_bonus,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Number of discrete actions in the action space.
|
||||
pub fn num_actions(&self) -> usize {
|
||||
@@ -999,6 +993,26 @@ pub struct DQNHyperparameters {
|
||||
/// are scaled by 1/(1-p) for expected-value correction. 0.0 = disabled.
|
||||
pub stochastic_depth_prob: f64,
|
||||
|
||||
/// Manifold Mixup alpha for Beta distribution.
|
||||
/// Mixes pairs of C51 target distributions during training.
|
||||
/// 0.0 = disabled. Range: [0.0, 2.0]. Default: 0.2.
|
||||
pub mixup_alpha: f64,
|
||||
|
||||
// ── Family intensity scalars for hyperopt (1.0 = defaults, range [0.0, 2.0]) ──
|
||||
|
||||
/// Adversarial family intensity: scales saboteur perturbation, warmup, interval.
|
||||
pub adversarial_intensity: f64,
|
||||
/// Regularization family intensity: scales dropout, spectral norm, stochastic depth, spectral decoupling.
|
||||
pub regularization_intensity: f64,
|
||||
/// Data augmentation family intensity: scales feature masking, feature noise.
|
||||
pub augmentation_intensity: f64,
|
||||
/// Loss shaping family intensity: scales asymmetric DD, regret blend, trade clustering, position entropy.
|
||||
pub loss_shaping_intensity: f64,
|
||||
/// Ensemble family intensity: scales ensemble disagreement penalty.
|
||||
pub ensemble_intensity: f64,
|
||||
/// Causal family intensity: scales causal weight, intervention interval.
|
||||
pub causal_intensity: f64,
|
||||
|
||||
/// #18 Asymmetric drawdown loss: extra penalty on Q-overestimation when
|
||||
/// portfolio is in drawdown. Weight for L_dd = max(0, Q_pred-Q_target) * dd^2.
|
||||
/// 0.0 = disabled.
|
||||
@@ -1036,6 +1050,11 @@ pub struct DQNHyperparameters {
|
||||
pub per_alpha: f64,
|
||||
pub per_beta_start: f64,
|
||||
|
||||
/// Regime-aware replay decay factor. PER priorities for cross-regime
|
||||
/// transitions are multiplied by decay^|regime_distance|.
|
||||
/// 1.0 = no bias. 0.0 = only current regime. Default: 0.3.
|
||||
pub regime_replay_decay: f64,
|
||||
|
||||
// Wave 2.1: Dueling Networks (always enabled)
|
||||
/// Hidden dimension for dueling value/advantage streams
|
||||
pub dueling_hidden_dim: usize,
|
||||
@@ -1200,6 +1219,10 @@ pub struct DQNHyperparameters {
|
||||
/// Spectral norm σ_max — constrains ||W||_σ ≤ σ_max.
|
||||
/// Range [1.0, 10.0]. Default 3.0 (permits Xavier scaling, prevents Q-explosion).
|
||||
pub spectral_norm_sigma_max: f64,
|
||||
/// Spectral decoupling: L2 penalty on Q-value logit magnitudes.
|
||||
/// Prevents overconfident Q-values. Pezeshki et al. 2021.
|
||||
/// Range: [0.0, 0.1]. Default: 0.01.
|
||||
pub spectral_decoupling_lambda: f64,
|
||||
/// When true, IQN's bounded Huber loss is used for PER priorities instead of
|
||||
/// C51's unbounded cross-entropy. Prevents the PER feedback explosion.
|
||||
|
||||
@@ -1363,11 +1386,6 @@ pub struct DQNHyperparameters {
|
||||
pub dt_target_return: f64,
|
||||
|
||||
// CVaR (Conditional Value at Risk) action selection for IQN
|
||||
/// Enable CVaR-aware action selection when IQN is active.
|
||||
/// When true, actions are selected by optimizing the worst-case quantile tail
|
||||
/// (risk-averse) instead of the mean quantile (risk-neutral).
|
||||
/// Default: true (enables risk-aware position scaling via IQN head).
|
||||
pub use_cvar_action_selection: bool,
|
||||
/// CVaR confidence level alpha (0.0-1.0). Lower alpha = more risk-averse.
|
||||
/// 0.05 = optimize for worst 5% outcomes (extreme caution).
|
||||
/// 0.25 = optimize for worst 25% outcomes (moderate risk aversion).
|
||||
@@ -1416,6 +1434,52 @@ impl DQNHyperparameters {
|
||||
(self.reward_scale / (1.0 - self.gamma) * 1.2).clamp(20.0, 300.0)
|
||||
}
|
||||
|
||||
/// Apply family intensity scalars to their respective parameters.
|
||||
/// Call after constructing from hyperopt PSO vector.
|
||||
/// Intensity 1.0 = no change. 0.5 = halved. 2.0 = doubled.
|
||||
/// For interval/epoch params: higher intensity = more frequent (divide by intensity).
|
||||
pub fn apply_family_scaling(&mut self) {
|
||||
// Adversarial family
|
||||
let ai = self.adversarial_intensity;
|
||||
self.adversarial_dd_threshold *= ai;
|
||||
if ai > 0.01 {
|
||||
self.adversarial_warmup_epochs = (self.adversarial_warmup_epochs as f64 / ai) as usize;
|
||||
self.adversarial_checkpoint_interval =
|
||||
(self.adversarial_checkpoint_interval as f64 / ai) as usize;
|
||||
}
|
||||
|
||||
// Regularization family
|
||||
let ri = self.regularization_intensity;
|
||||
self.dropout_initial = (self.dropout_initial * ri).clamp(0.0, 0.5);
|
||||
self.spectral_norm_sigma_max *= ri;
|
||||
self.stochastic_depth_prob = (self.stochastic_depth_prob * ri).clamp(0.0, 0.5);
|
||||
self.spectral_decoupling_lambda *= ri;
|
||||
|
||||
// Data augmentation family
|
||||
let di = self.augmentation_intensity;
|
||||
self.feature_mask_fraction = (self.feature_mask_fraction * di).clamp(0.0, 0.8);
|
||||
self.feature_noise_scale *= di;
|
||||
|
||||
// Loss shaping family
|
||||
let li = self.loss_shaping_intensity;
|
||||
self.asymmetric_dd_weight *= li;
|
||||
self.regret_blend = (self.regret_blend * li).clamp(0.0, 1.0);
|
||||
self.trade_clustering_penalty *= li;
|
||||
self.position_entropy_weight *= li;
|
||||
|
||||
// Ensemble family
|
||||
let ei = self.ensemble_intensity;
|
||||
self.ensemble_disagreement_penalty *= ei;
|
||||
|
||||
// Causal family
|
||||
let ci = self.causal_intensity;
|
||||
self.causal_weight *= ci;
|
||||
if ci > 0.01 {
|
||||
self.causal_intervention_interval =
|
||||
(self.causal_intervention_interval as f64 / ci) as usize;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create conservative hyperparameters suitable for testing and development.
|
||||
/// WARNING: These are NOT optimized for production. Use hyperopt results instead.
|
||||
/// After DQN hyperopt completes, update ml/hyperparams/dqn_best.toml with optimal values.
|
||||
@@ -1525,6 +1589,14 @@ impl DQNHyperparameters {
|
||||
enable_gradient_vaccine: true, // #32: mathematical overfit guarantee (one production path)
|
||||
bottleneck_dim: 2, // #31: 2D information compression (gem of gems)
|
||||
stochastic_depth_prob: 0.2, // #21: 20% drop probability per hidden layer
|
||||
mixup_alpha: 0.2, // Manifold Mixup: Beta(0.2,0.2) target distribution mixing
|
||||
// Family intensity scalars: 1.0 = no change to defaults (neutral)
|
||||
adversarial_intensity: 1.0,
|
||||
regularization_intensity: 1.0,
|
||||
augmentation_intensity: 1.0,
|
||||
loss_shaping_intensity: 1.0,
|
||||
ensemble_intensity: 1.0,
|
||||
causal_intensity: 1.0,
|
||||
asymmetric_dd_weight: 0.5, // #18: extra loss on Q-overestimation in drawdown
|
||||
|
||||
// Wave 16 Portfolio Features (default: ALL ENABLED)
|
||||
@@ -1549,6 +1621,7 @@ impl DQNHyperparameters {
|
||||
|
||||
per_alpha: 0.6,
|
||||
per_beta_start: 0.6, // Higher start → faster IS weight correction (was 0.4)
|
||||
regime_replay_decay: 0.3, // Regime-aware replay: bias toward current regime
|
||||
|
||||
// Wave 2.1: Dueling Networks (WAVE 6.4: ENABLED BY DEFAULT)
|
||||
dueling_hidden_dim: 128, // Default: 128 hidden units
|
||||
@@ -1635,6 +1708,7 @@ impl DQNHyperparameters {
|
||||
qr_kappa: 1.0, // Default: 1.0 (standard quantile Huber loss)
|
||||
iqn_lambda: 0.25, // Default: mild IQN regularization alongside C51
|
||||
spectral_norm_sigma_max: 3.0, // Default: permits Xavier scaling [1.0, 10.0]
|
||||
spectral_decoupling_lambda: 0.01, // Default: mild logit magnitude penalty (Pezeshki 2021)
|
||||
|
||||
// Phase 3: GPU experience collection
|
||||
gpu_n_episodes: 256, // Floor: optimal_n_episodes() scales up dynamically (H100→8192)
|
||||
@@ -1709,8 +1783,6 @@ impl DQNHyperparameters {
|
||||
dt_num_layers: 3,
|
||||
dt_target_return: 2.0,
|
||||
|
||||
// CVaR action selection: enabled by default (risk-aware IQN action scoring)
|
||||
use_cvar_action_selection: true,
|
||||
cvar_alpha: 0.05, // Optimize for worst 5% quantile tail
|
||||
|
||||
// Contract specification: ES futures defaults
|
||||
@@ -1801,7 +1873,6 @@ pub(crate) fn dqn_default_config() -> DQNConfig {
|
||||
|
||||
// Target Network Updates
|
||||
target_update_freq: 1,
|
||||
use_soft_updates: true,
|
||||
tau: 0.001,
|
||||
tau_final: 0.0001,
|
||||
tau_anneal_steps: 100_000,
|
||||
@@ -1815,7 +1886,6 @@ pub(crate) fn dqn_default_config() -> DQNConfig {
|
||||
n_steps: 3,
|
||||
|
||||
// Stability & Safety
|
||||
enable_q_value_clipping: true,
|
||||
q_value_clip_min: -5.0, // Reward v4: Q-values in [-0.5, +0.5], 10x headroom
|
||||
q_value_clip_max: 5.0,
|
||||
gradient_collapse_multiplier: 100.0,
|
||||
@@ -1826,11 +1896,9 @@ pub(crate) fn dqn_default_config() -> DQNConfig {
|
||||
|
||||
// CQL + IQN (2026 modernization)
|
||||
cql_alpha: 1.0,
|
||||
use_iqn: true,
|
||||
iqn_num_quantiles: 64,
|
||||
iqn_kappa: 1.0,
|
||||
iqn_embedding_dim: 64,
|
||||
use_cvar_action_selection: true,
|
||||
cvar_alpha: 0.05,
|
||||
|
||||
minimum_profit_factor: 1.5,
|
||||
|
||||
@@ -201,6 +201,8 @@ impl FusedTrainingCtx {
|
||||
weight_decay: hyperparams.weight_decay as f32,
|
||||
max_grad_norm: resolved_grad_norm as f32,
|
||||
spectral_norm_sigma_max: hyperparams.spectral_norm_sigma_max as f32,
|
||||
spectral_decoupling_lambda: hyperparams.spectral_decoupling_lambda as f32,
|
||||
mixup_alpha: hyperparams.mixup_alpha as f32,
|
||||
iqn_lambda: hyperparams.iqn_lambda as f32,
|
||||
iqn_num_quantiles: if hyperparams.iqn_lambda > 0.0 { hyperparams.num_quantiles } else { 0 },
|
||||
iqn_embedding_dim: dqn.config.iqn_embedding_dim,
|
||||
@@ -634,20 +636,18 @@ impl FusedTrainingCtx {
|
||||
{
|
||||
let dqn = agent.primary_dqn_mut();
|
||||
|
||||
if dqn.config.use_soft_updates {
|
||||
let tau = compute_cosine_annealed_tau(
|
||||
dqn.get_training_steps(),
|
||||
dqn.config.tau,
|
||||
dqn.config.tau_final,
|
||||
dqn.config.tau_anneal_steps,
|
||||
);
|
||||
let tau = compute_cosine_annealed_tau(
|
||||
dqn.get_training_steps(),
|
||||
dqn.config.tau,
|
||||
dqn.config.tau_final,
|
||||
dqn.config.tau_anneal_steps,
|
||||
);
|
||||
|
||||
self.trainer.target_ema_update(
|
||||
&self.online_dueling, &self.online_branching,
|
||||
&mut self.target_dueling, &mut self.target_branching,
|
||||
tau as f32,
|
||||
).map_err(|e| anyhow::anyhow!("GPU EMA target update: {e}"))?;
|
||||
}
|
||||
self.trainer.target_ema_update(
|
||||
&self.online_dueling, &self.online_branching,
|
||||
&mut self.target_dueling, &mut self.target_branching,
|
||||
tau as f32,
|
||||
).map_err(|e| anyhow::anyhow!("GPU EMA target update: {e}"))?;
|
||||
}
|
||||
|
||||
// ── Step 3b: Attention forward + backward + Adam (post-graph) ─────
|
||||
@@ -739,16 +739,14 @@ impl FusedTrainingCtx {
|
||||
|
||||
// IQN target EMA (same schedule as DQN)
|
||||
let dqn = agent.primary_dqn_mut();
|
||||
if dqn.config.use_soft_updates {
|
||||
let tau = compute_cosine_annealed_tau(
|
||||
dqn.get_training_steps(),
|
||||
dqn.config.tau,
|
||||
dqn.config.tau_final,
|
||||
dqn.config.tau_anneal_steps,
|
||||
);
|
||||
iqn.target_ema_update(tau as f32)
|
||||
.map_err(|e| anyhow::anyhow!("IQN EMA update: {e}"))?;
|
||||
}
|
||||
let tau = compute_cosine_annealed_tau(
|
||||
dqn.get_training_steps(),
|
||||
dqn.config.tau,
|
||||
dqn.config.tau_final,
|
||||
dqn.config.tau_anneal_steps,
|
||||
);
|
||||
iqn.target_ema_update(tau as f32)
|
||||
.map_err(|e| anyhow::anyhow!("IQN EMA update: {e}"))?;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("IQN step failed (non-fatal): {e}");
|
||||
|
||||
@@ -24,7 +24,6 @@ use crate::dqn::reward::{RewardConfig, RewardFunction};
|
||||
use crate::features::microstructure_features::*;
|
||||
use crate::labeling::triple_barrier::TripleBarrierEngine;
|
||||
use crate::memory_optimization::auto_batch_size::{AutoBatchSizer, BatchSizeConfig};
|
||||
use crate::trainers::TargetUpdateMode;
|
||||
use crate::TrainingMetrics;
|
||||
use super::super::config::{DQNAgentType, DQNHyperparameters};
|
||||
use super::DQNTrainer;
|
||||
@@ -268,7 +267,6 @@ impl DQNTrainer {
|
||||
tau: hyperparams.tau,
|
||||
tau_final: hyperparams.tau * 0.1, // Anneal to 10% of base tau
|
||||
tau_anneal_steps: 100_000,
|
||||
use_soft_updates: matches!(hyperparams.target_update_mode, TargetUpdateMode::Soft),
|
||||
|
||||
// Rainbow DQN warmup period
|
||||
warmup_steps: hyperparams.warmup_steps,
|
||||
@@ -297,7 +295,6 @@ impl DQNTrainer {
|
||||
noisy_sigma_init: hyperparams.noisy_sigma_init, // Rainbow DQN standard: 0.5
|
||||
|
||||
// BUG #37 FIX: Q-value clipping (prevents step-level explosions)
|
||||
enable_q_value_clipping: true,
|
||||
q_value_clip_min: -500.0,
|
||||
q_value_clip_max: 500.0,
|
||||
|
||||
@@ -312,7 +309,6 @@ impl DQNTrainer {
|
||||
iqn_lambda: hyperparams.iqn_lambda as f32, // IQN dual-head loss weight (f64→f32)
|
||||
branch_hidden_dim: hyperparams.branch_hidden_dim,
|
||||
use_regime_conditioning: true, // Always enable per-regime IS weights for branching loss
|
||||
use_cvar_action_selection: hyperparams.use_cvar_action_selection,
|
||||
cvar_alpha: hyperparams.cvar_alpha,
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
@@ -321,7 +317,6 @@ impl DQNTrainer {
|
||||
dropout_rate: if hyperparams.enable_dropout_scheduler { hyperparams.dropout_initial } else { 0.0 },
|
||||
entropy_coefficient: hyperparams.entropy_coefficient,
|
||||
noisy_epsilon_floor: hyperparams.noisy_epsilon_floor.unwrap_or(0.0) as f32, // C2: NoisyNet handles exploration
|
||||
use_count_bonus: hyperparams.count_bonus_coefficient.unwrap_or(0.0) > 0.0, // C3 FIX: enable when coefficient > 0
|
||||
count_bonus_coefficient: hyperparams.count_bonus_coefficient.unwrap_or(0.0),
|
||||
..DQNConfig::default()
|
||||
};
|
||||
|
||||
@@ -570,7 +570,6 @@ fn test_c3_search_space_is_22d() {
|
||||
assert!(!names.contains(&"branch_hidden_dim"), "branch_hidden_dim should be fixed, not in search space");
|
||||
assert!(!names.contains(&"curiosity_weight"), "curiosity_weight should be fixed, not in search space");
|
||||
assert!(!names.contains(&"her_ratio"), "her_ratio should be fixed, not in search space");
|
||||
assert!(!names.contains(&"use_cvar_action_selection"), "use_cvar_action_selection should be fixed, not in search space");
|
||||
assert!(!names.contains(&"cvar_alpha"), "cvar_alpha should be fixed, not in search space");
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ pub struct ReplayBufferSection {
|
||||
pub min_replay_size: Option<usize>,
|
||||
pub per_alpha: Option<f64>,
|
||||
pub per_beta_start: Option<f64>,
|
||||
pub regime_replay_decay: Option<f64>,
|
||||
}
|
||||
|
||||
/// C51 distributional RL parameters.
|
||||
@@ -179,6 +180,14 @@ pub struct GeneralizationSection {
|
||||
pub position_entropy_weight: Option<f64>,
|
||||
pub trade_clustering_penalty: Option<f64>,
|
||||
pub ensemble_disagreement_penalty: Option<f64>,
|
||||
pub mixup_alpha: Option<f64>,
|
||||
// Family intensity scalars for hyperopt (1.0 = defaults, range [0.0, 2.0])
|
||||
pub adversarial_intensity: Option<f64>,
|
||||
pub regularization_intensity: Option<f64>,
|
||||
pub augmentation_intensity: Option<f64>,
|
||||
pub loss_shaping_intensity: Option<f64>,
|
||||
pub ensemble_intensity: Option<f64>,
|
||||
pub causal_intensity: Option<f64>,
|
||||
}
|
||||
|
||||
/// Risk management and position-control parameters.
|
||||
@@ -750,6 +759,9 @@ impl DqnTrainingProfile {
|
||||
if let Some(v) = rb.per_beta_start {
|
||||
hp.per_beta_start = v;
|
||||
}
|
||||
if let Some(v) = rb.regime_replay_decay {
|
||||
hp.regime_replay_decay = v;
|
||||
}
|
||||
}
|
||||
|
||||
// [distributional]
|
||||
@@ -839,6 +851,14 @@ impl DqnTrainingProfile {
|
||||
if let Some(v) = g.position_entropy_weight { hp.position_entropy_weight = v; }
|
||||
if let Some(v) = g.trade_clustering_penalty { hp.trade_clustering_penalty = v; }
|
||||
if let Some(v) = g.ensemble_disagreement_penalty { hp.ensemble_disagreement_penalty = v; }
|
||||
if let Some(v) = g.mixup_alpha { hp.mixup_alpha = v; }
|
||||
// Family intensity scalars
|
||||
if let Some(v) = g.adversarial_intensity { hp.adversarial_intensity = v; }
|
||||
if let Some(v) = g.regularization_intensity { hp.regularization_intensity = v; }
|
||||
if let Some(v) = g.augmentation_intensity { hp.augmentation_intensity = v; }
|
||||
if let Some(v) = g.loss_shaping_intensity { hp.loss_shaping_intensity = v; }
|
||||
if let Some(v) = g.ensemble_intensity { hp.ensemble_intensity = v; }
|
||||
if let Some(v) = g.causal_intensity { hp.causal_intensity = v; }
|
||||
}
|
||||
|
||||
// [risk]
|
||||
|
||||
@@ -222,7 +222,6 @@ mod tests {
|
||||
config.batch_size = 4;
|
||||
config.min_replay_size = 4;
|
||||
config.warmup_steps = 0;
|
||||
config.use_iqn = false;
|
||||
config.epsilon_start = 0.5;
|
||||
config
|
||||
}
|
||||
|
||||
@@ -88,7 +88,6 @@ fn test_full_iqn_cql_training_loop() {
|
||||
config.state_dim = 8;
|
||||
config.num_actions = 3;
|
||||
config.hidden_dims = vec![32, 16];
|
||||
config.use_iqn = true;
|
||||
config.iqn_num_quantiles = 16;
|
||||
config.cql_alpha = 1.0;
|
||||
config.batch_size = 8;
|
||||
@@ -139,7 +138,6 @@ fn test_iqn_only_no_cql() {
|
||||
config.state_dim = 8;
|
||||
config.num_actions = 3;
|
||||
config.hidden_dims = vec![16, 16];
|
||||
config.use_iqn = true;
|
||||
config.iqn_num_quantiles = 8;
|
||||
config.batch_size = 4;
|
||||
config.min_replay_size = 4;
|
||||
@@ -168,11 +166,9 @@ fn test_cvar_action_selection_integration() {
|
||||
config.state_dim = 8;
|
||||
config.num_actions = 3;
|
||||
config.hidden_dims = vec![16, 16];
|
||||
config.use_iqn = true;
|
||||
config.iqn_num_quantiles = 8;
|
||||
config.epsilon_start = 0.0;
|
||||
config.warmup_steps = 0;
|
||||
config.use_cvar_action_selection = true;
|
||||
config.cvar_alpha = 0.05;
|
||||
|
||||
let mut dqn = DQN::new(config).unwrap();
|
||||
|
||||
1023
docs/superpowers/plans/2026-03-30-dqn-gems-implementation.md
Normal file
1023
docs/superpowers/plans/2026-03-30-dqn-gems-implementation.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user