khtst-rust / src /controlador.rs
PowerMachine's picture
v0.9.0: análise/prova/teste das críticas Qwen3.8 lote 2 (r-34) — SIMD com despacho em runtime bit-a-bit (X-1), oráculo candle_nn::optim::AdamW (nosso 1,53× mais rápido), Thompson Sampling (regret 0,0019 vs UCB1 0,0156), GNG por entropia com piso (T-G1'), JEPA de fração adaptativa (bit-a-bit com 1 braço), configs tipadas, entrada numpy (2008× na fronteira), avaliação com dados reais — 104/104 gates, 111/111 cargo
d444358 verified
Raw History Blame Contribute Delete
34 kB
//! Controlador de hiperparâmetros automatizado (pedido do usuário: "ajustes
//! automatizados de todos hiper parâmetros"). TODA decisão é registrada na
//! telemetria com motivo (auditável).
//!
//! - LR: aquecimento → cosseno; plateau (paciência) → ×fator; divergência
//! (NaN ou perda > 2× mínimo EMA) → ×0,3 + rollback do estado;
//! - λ (âncora, Teo 29.2): hipergradiente exato;
//! - ε da corrupção (Teo C.3(ii)): calibra pela taxa de erro por nível em
//! validação (cobertura — "calibrar ρ tal que erro ≤ ρ");
//! - clip: adapta ao percentil da norma de gradiente;
//! - τ do GSQ: agenda analítica (Teo 30.6) — no tokenizador.
use crate::telemetria::TelemetryHub;
pub struct Controlador {
pub lr_base: f64,
pub lr_corrente: f64,
pub aquecimento: usize,
pub passos_totais: usize,
pub paciencia_plateau: usize,
pub fator_plateau: f64,
pub minimo_ema: f64,
pub passos_sem_melhora: usize,
pub clip: f64,
pub clip_percentil: f64,
pub normas_grad: Vec<f64>,
pub lambda_ancora: crate::perdas::LambdaAncora,
pub epsilon_corrupcao: f64,
}
impl Controlador {
pub fn novo(lr_base: f64, aquecimento: usize, passos_totais: usize, paciencia: usize, fator: f64, rho_ancora: f64) -> Self {
Controlador {
lr_base,
lr_corrente: lr_base,
aquecimento,
passos_totais,
paciencia_plateau: paciencia,
fator_plateau: fator,
minimo_ema: f64::INFINITY,
passos_sem_melhora: 0,
clip: 1.0,
clip_percentil: 90.0,
normas_grad: Vec::new(),
lambda_ancora: crate::perdas::LambdaAncora::novo(rho_ancora),
epsilon_corrupcao: 0.15,
}
}
/// LR do passo (aquecimento → cosseno com decaimentos por plateau).
pub fn lr_passo(&mut self, passo: usize, perda: f64, hub: &mut TelemetryHub) -> f64 {
// divergência?
if !perda.is_finite() || perda > 2.0 * self.minimo_ema {
let de = self.lr_corrente;
self.lr_corrente *= 0.3;
hub.ajuste("lr", de, self.lr_corrente,
&format!("divergência detectada (perda {perda:.3e} vs mínimo EMA {:.3e})", self.minimo_ema));
self.passos_sem_melhora = 0;
return self.lr_corrente;
}
// rastreio de mínimo (EMA implícito pela suavização do próprio valor)
if perda < self.minimo_ema * 0.999 {
self.minimo_ema = self.minimo_ema.min(perda);
self.passos_sem_melhora = 0;
} else {
self.passos_sem_melhora += 1;
}
// plateau
if self.paciencia_plateau > 0 && self.passos_sem_melhora >= self.paciencia_plateau {
let de = self.lr_corrente;
self.lr_corrente *= self.fator_plateau;
hub.ajuste("lr", de, self.lr_corrente,
&format!("plateau: {} passos sem melhora", self.passos_sem_melhora));
self.passos_sem_melhora = 0;
}
// programação base: aquecimento + cosseno
let p = passo as f64;
let lr_prog = if p < self.aquecimento as f64 {
self.lr_base * (p + 1.0) / self.aquecimento.max(1) as f64
} else {
let t = (p - self.aquecimento as f64) / (self.passos_totais_clamped() - self.aquecimento).max(1) as f64;
let cosseno = 0.5 * (1.0 + (std::f64::consts::PI * t.min(1.0)).cos());
self.lr_base * (0.1 + 0.9 * cosseno)
};
// o menor entre a programação e o teto pós-plateau
self.lr_corrente = self.lr_corrente.min(lr_prog).max(self.lr_base * 0.01);
self.lr_corrente
}
fn passos_totais_clamped(&self) -> usize {
self.passos_totais.max(self.aquecimento + 1)
}
/// Clip adaptativo: percentil das normas recentes (mín. 0,25).
pub fn clip_corrente(&mut self, hub: &mut TelemetryHub) -> f64 {
if self.normas_grad.len() >= 32 {
let mut v = self.normas_grad.clone();
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
let idx = ((self.clip_percentil / 100.0) * (v.len() - 1) as f64).round() as usize;
let novo = v[idx.min(v.len() - 1)].max(0.25);
if (novo - self.clip).abs() / self.clip > 0.3 {
hub.ajuste("clip", self.clip, novo, "percentil 90 das normas de gradiente (janela 32)");
self.clip = novo;
}
}
self.clip
}
pub fn registrar_norma(&mut self, n: f64) {
self.normas_grad.push(n);
if self.normas_grad.len() > 256 {
self.normas_grad.remove(0);
}
}
/// ε da corrupção (C.3(ii)): sobe se a taxa de erro por nível em validação
/// excede ε (cobertura insuficiente), desce lentamente caso contrário.
pub fn calibrar_epsilon(&mut self, taxa_erro_por_nivel: f64, hub: &mut TelemetryHub) -> f64 {
let de = self.epsilon_corrupcao;
if taxa_erro_por_nivel > self.epsilon_corrupcao + 0.02 {
self.epsilon_corrupcao = (self.epsilon_corrupcao + 0.02).min(0.35);
} else if taxa_erro_por_nivel < self.epsilon_corrupcao * 0.5 {
self.epsilon_corrupcao = (self.epsilon_corrupcao - 0.01).max(0.05);
}
if (de - self.epsilon_corrupcao).abs() > 1e-9 {
hub.ajuste("corrupcao_epsilon", de, self.epsilon_corrupcao,
&format!("cobertura C.3(ii): taxa de erro por nível medida {:.3}", taxa_erro_por_nivel));
}
self.epsilon_corrupcao
}
}
// ---------------------------------------------------------------------------
// v0.6.0 — AUTOAJUSTE DO PENSAMENTO POR TELEMETRIA (r-21 §3.5 fechado)
// ---------------------------------------------------------------------------
/// Regras de autoajuste dos hiperparâmetros do Pensamento, CADA UMA com
/// garantia matemática (provas completas em r-26):
///
/// - **A-1 (τ_c por quantil DKW):** `τ_c = quantil_{1−ε}` das confianças
/// calibradas retidas ⇒ taxa de erro do "aceitar" ≤ ε + 2e^{−2nδ²}
/// (Dvoretzky–Kiefer–Wolfowitz–Massart).
/// - **A-2 (B por análise marginal):** o ganho esperado por iteração decai
/// geometricamente (T-P4: potencial Φ = B − k); com ganho inicial Δ e
/// razão ρ̂ medida na telemetria, o B ótimo minimiza
/// `Custo(B) = c_iter·B − Δ·Σ_{k<B} ρ̂^k = c_iter·B − Δ(1−ρ̂^B)/(1−ρ̂)`
/// ⇒ `B* = ceil(log(c_iter(1−ρ̂)/Δ)/log(ρ̂))` quando a razão existe.
/// - **A-3 (N por Hoeffding + lema do acordo):** tentativas i.i.d. com
/// correção p ⇒ acordo par-a-par esperado `a = p² + (1−p)² = 1 − 2p(1−p)`
/// ⇒ `p̂ = (1 + √(2â − 1))/2` (raiz ≥ 1/2 da quadrática); para erro de
/// maioria ≤ δ: `N ≥ ln(1/δ) / (2(p̂ − 1/2)²)` (Hoeffding).
/// - **A-5 (g por Bernstein empírico):** o gate sobe somente se o LIMITE
/// INFERIOR do ganho medido é positivo:
/// `média − √(2·variância·ln(2/α)/n) − 7ln(2/α)/(3(n−1)) > 0`.
pub struct AutoAjustePensamento {
pub tau_c: f64,
pub orcamento_b: usize,
pub n_tentativas: usize,
pub gate_g: f64,
/// histórico de confianças calibradas retidas (para A-1)
pub confs_retidas: Vec<f64>,
/// ganho de confiança por iteração do ciclo d (para A-2)
pub ganhos_por_iteracao: Vec<f64>,
/// acordos par-a-par observados (para A-3)
pub acordos: Vec<f64>,
/// ganhos atribuídos ao gate (para A-5)
pub ganhos_gate: Vec<f64>,
}
impl AutoAjustePensamento {
pub fn novo(tau_c_inicial: f64, b_inicial: usize, n_inicial: usize) -> Self {
AutoAjustePensamento {
tau_c: tau_c_inicial,
orcamento_b: b_inicial,
n_tentativas: n_inicial,
gate_g: 0.0,
confs_retidas: Vec::new(),
ganhos_por_iteracao: Vec::new(),
acordos: Vec::new(),
ganhos_gate: Vec::new(),
}
}
/// **A-1:** reajusta τ_c pelo quantil 1−ε das confianças retidas.
pub fn ajustar_tau_c(&mut self, epsilon: f64, hub: &mut TelemetryHub) -> f64 {
if self.confs_retidas.len() < 32 {
return self.tau_c; // amostra insuficiente — mantém (honesto)
}
let mut v = self.confs_retidas.clone();
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
let idx = (((1.0 - epsilon.clamp(0.0, 0.5)) * v.len() as f64).ceil() as usize)
.saturating_sub(1)
.min(v.len() - 1);
let novo = v[idx];
if (novo - self.tau_c).abs() > 1e-6 {
hub.ajuste("pensamento_tau_c", self.tau_c, novo, &format!(
"A-1 quantil DKW 1−ε (ε={epsilon:.2}, n={})", v.len()));
self.tau_c = novo;
}
self.tau_c
}
/// **A-2:** reajusta o orçamento B pela análise marginal do ganho medido.
pub fn ajustar_orcamento(&mut self, custo_iter_relativo: f64, hub: &mut TelemetryHub) -> usize {
if self.ganhos_por_iteracao.len() < 8 {
return self.orcamento_b;
}
// razão geométrica média ρ̂ entre ganhos consecutivos positivos
let mut razoes = Vec::new();
for w in self.ganhos_por_iteracao.windows(2) {
if w[0] > 1e-9 && w[1] >= 0.0 {
razoes.push(((w[1] / w[0]) as f64).clamp(0.0, 0.999));
}
}
if razoes.len() < 4 {
return self.orcamento_b;
}
razoes.sort_by(|a, b| a.partial_cmp(b).unwrap());
let rho = razoes[razoes.len() / 2]; // mediana (robusto)
let delta0 = self.ganhos_por_iteracao.iter().cloned().fold(f64::MIN, f64::max).max(1e-9);
// B* = ceil( ln(c·(1−ρ)/Δ) / ln(ρ) ) — ótimo do custo marginal
let b_otimo = ((custo_iter_relativo * (1.0 - rho) / delta0).ln() / rho.ln())
.ceil()
.clamp(1.0, 16.0) as usize;
if b_otimo != self.orcamento_b {
hub.ajuste("pensamento_orcamento_b", self.orcamento_b as f64, b_otimo as f64, &format!(
"A-2 análise marginal (ρ̂={rho:.3}, Δ₀={delta0:.4}, c={custo_iter_relativo:.3})"));
self.orcamento_b = b_otimo;
}
self.orcamento_b
}
/// **A-3:** reajusta N pelo estimador do acordo + Hoeffding.
pub fn ajustar_n(&mut self, delta_erro: f64, n_max: usize, hub: &mut TelemetryHub) -> usize {
if self.acordos.len() < 16 {
return self.n_tentativas;
}
let mut v = self.acordos.clone();
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
let a_med = v[v.len() / 2];
// lema do acordo: a = p² + (1−p)² ⇒ p = (1 + √(2a−1))/2 (a ≥ 1/2)
let p_chapeu = if a_med >= 0.5 {
(1.0 + (2.0 * a_med - 1.0).sqrt().min(1.0)) / 2.0
} else {
0.5 // acordo < 1/2: tentativas anti-correlacionadas — p̂ = 1/2 (pior caso honesto)
};
let margem = (p_chapeu - 0.5).max(1e-3);
// N ≥ ln(1/δ) / (2(p̂−1/2)²) — note ln(1/δ) = −ln(δ) > 0 para δ < 1
let n_necessario = ((1.0 / delta_erro).ln() / (2.0 * margem * margem)).ceil().max(1.0) as usize;
let novo = n_necessario.clamp(1, n_max);
if novo != self.n_tentativas {
hub.ajuste("pensamento_n_tentativas", self.n_tentativas as f64, novo as f64, &format!(
"A-3 Hoeffding+acordo (â={a_med:.3} ⇒ p̂={p_chapeu:.3}, δ={delta_erro:.2})"));
self.n_tentativas = novo;
}
self.n_tentativas
}
/// **A-5:** gate sobe somente com LCB positivo (Bernstein empírico).
pub fn ajustar_gate(&mut self, ganho_amostral: f64, alfa: f64, hub: &mut TelemetryHub) -> f64 {
self.ganhos_gate.push(ganho_amostral);
if self.ganhos_gate.len() < 16 {
return self.gate_g;
}
let n = self.ganhos_gate.len() as f64;
let media: f64 = self.ganhos_gate.iter().sum::<f64>() / n;
let var: f64 =
self.ganhos_gate.iter().map(|x| (x - media) * (x - media)).sum::<f64>() / (n - 1.0);
let lcb = media
- (2.0 * var * (2.0 / alfa).ln() / n).sqrt()
- 7.0 * (2.0 / alfa).ln() / (3.0 * (n - 1.0));
if lcb > 0.0 {
let novo = (self.gate_g + 0.1).min(1.0);
if novo != self.gate_g {
hub.ajuste("pensamento_gate_g", self.gate_g, novo, &format!(
"A-5 Bernstein empírico (LCB={lcb:.4} > 0, média={media:.4})"));
self.gate_g = novo;
}
} else if lcb < -0.05 && self.gate_g > 0.0 {
let novo = (self.gate_g - 0.1).max(0.0);
hub.ajuste("pensamento_gate_g", self.gate_g, novo, &format!(
"A-5 Bernstein empírico (LCB={lcb:.4} < 0 — recua)"));
self.gate_g = novo;
}
self.gate_g
}
}
// ---------------------------------------------------------------------------
// v0.6.0 — seleção de arquitetura por UCB1 (A-4)
// ---------------------------------------------------------------------------
/// **A-4 (UCB1 — Auer et al. 2002):** braços = variantes de arquitetura;
/// recompensa = mécanismo de qualidade medido na telemetria. O seletor
/// joga o braço com maior `r̄_a + √(2 ln T / n_a)`; arrependimento
/// `R_T ≤ Σ_{a≠*} 8 ln T / Δ_a + (1 + π²/3)` — sublinear em T.
pub struct Ucb1 {
pub nomes: Vec<String>,
pub somas: Vec<f64>,
pub contagens: Vec<u64>,
pub t: u64,
}
impl Ucb1 {
pub fn novo(nomes: Vec<String>) -> Self {
let n = nomes.len();
Ucb1 { nomes, somas: vec![0.0; n], contagens: vec![0; n], t: 0 }
}
/// Escolhe o braço (cada braço é jogado 1× antes da exploração).
pub fn escolher(&self) -> usize {
for (i, &c) in self.contagens.iter().enumerate() {
if c == 0 {
return i;
}
}
let ln_t = (self.t as f64).ln();
let mut melhor = 0usize;
let mut melhor_ucb = f64::NEG_INFINITY;
for i in 0..self.nomes.len() {
let media = self.somas[i] / self.contagens[i] as f64;
let ucb = media + (2.0 * ln_t / self.contagens[i] as f64).sqrt();
if ucb > melhor_ucb {
melhor_ucb = ucb;
melhor = i;
}
}
melhor
}
pub fn registrar(&mut self, braco: usize, recompensa: f64) {
self.somas[braco] += recompensa;
self.contagens[braco] += 1;
self.t += 1;
}
pub fn medias(&self) -> Vec<f64> {
self.contagens
.iter()
.zip(self.somas.iter())
.map(|(&c, &s)| if c > 0 { s / c as f64 } else { 0.0 })
.collect()
}
/// Arrependimento empírico vs. melhor braço a posteriori (auditoria).
pub fn arrependimento_empirico(&self) -> f64 {
let medias = self.medias();
let melhor = medias.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
self.contagens
.iter()
.zip(medias.iter())
.map(|(&c, &m)| c as f64 * (melhor - m))
.sum()
}
}
// ---------------------------------------------------------------------------
// v0.9.0 — Thompson Sampling (crítica Qwen3.8 C; provas T-TS1/T-TS2 em
// r-34 §7) — alternativa À UCB1, aditiva (nenhuma removida)
// ---------------------------------------------------------------------------
/// **T-TS1 (Thompson Sampling Beta-Bernoulli):** mantém posteriores
/// Beta(α_a, β_a) por braço; a cada passo amostra θ_a ~ Beta(α_a, β_a) e joga
/// argmax θ_a. Propriedades:
///
/// 1. **Posterior exata**: com prior Beta(1,1) e recompensas Bernoulli, a
/// posterior é Beta(1+s_a, 1+f_a) — conjugação clássica;
/// 2. **Concentração**: α_a/(α_a+β_a) → p_a com desvio O(√(ln t/t)) — a
/// variância da Beta decresce como 1/(α+β), e α+β cresce linearmente nos
/// jogados;
/// 3. **Arrependimento logarítmico assintótico** (Agrawal & Goyal 2012,
/// teorema 1 para Beta-Bernoulli TS): E[R_T] = O(Σ_a Δ_a·ln T/kl_a) —
/// instância-ótimo no regime assintótico (constantes melhores que UCB1
/// na prática; UCB1 tem a cota finita-tempo limpa de Auer — os dois
/// coexistem aqui);
/// 4. **Recompensas contínuas** via Bernoulli-ização: r ∈ [0,1] vira
/// sucesso ~ Bernoulli(r) (amostra u ~ U(0,1), sucesso ⟺ u < r) — a
/// técnica padrão que preserva as garantias de arrependimento para
/// recompensas em [0,1] (Agrawal & Goyal §1.2).
///
/// **Determinismo**: o amostrador é o `RngDeterministico` semeado — mesma
/// semente ⇒ mesma trajetória bit-a-bit (compatível com os gates S4-2).
pub struct ThompsonSampling {
pub nomes: Vec<String>,
pub alfas: Vec<f64>,
pub betas: Vec<f64>,
pub t: u64,
rng: crate::rng::RngDeterministico,
/// nº de amostragens que caíram no braço argmax (telemetria de
/// convergência do posterior).
pub sucessos: Vec<u64>,
pub jogadas: Vec<u64>,
}
impl ThompsonSampling {
pub fn novo(nomes: Vec<String>, semente: u64) -> Self {
let n = nomes.len();
ThompsonSampling {
nomes,
alfas: vec![1.0; n],
betas: vec![1.0; n],
t: 0,
rng: crate::rng::RngDeterministico::novo(semente),
sucessos: vec![0; n],
jogadas: vec![0; n],
}
}
/// Amostra θ_a ~ Beta(α_a, β_a) de cada braço e devolve o argmax
/// (desempate pelo menor índice — determinístico dado o RNG).
pub fn escolher(&mut self) -> usize {
let mut melhor = 0usize;
let mut melhor_theta = f64::NEG_INFINITY;
for i in 0..self.alfas.len() {
let theta = self.rng.beta(self.alfas[i], self.betas[i]);
if theta > melhor_theta {
melhor_theta = theta;
melhor = i;
}
}
melhor
}
/// Registra recompensa Bernoulli (sucesso/falha) — atualização conjugada.
pub fn registrar(&mut self, braco: usize, sucesso: bool) {
if sucesso {
self.alfas[braco] += 1.0;
self.sucessos[braco] += 1;
} else {
self.betas[braco] += 1.0;
}
self.jogadas[braco] += 1;
self.t += 1;
}
/// Recompensa contínua r ∈ [0,1] via Bernoulli-ização (T-TS1.4) —
/// preserva as garantias de arrependimento e mantém o posterior exato
/// na família Beta.
pub fn registrar_continuo(&mut self, braco: usize, recompensa: f64) {
let r = recompensa.clamp(0.0, 1.0);
let sucesso = self.rng.uniforme() < r;
self.registrar(braco, sucesso);
}
/// Médias posteriores α/(α+β) (estimativa pontual de p_a).
pub fn medias_posteriori(&self) -> Vec<f64> {
self.alfas
.iter()
.zip(self.betas.iter())
.map(|(&a, &b)| a / (a + b))
.collect()
}
/// Arrependimento empírico vs. melhor braço a posteriori (mesma métrica
/// de auditoria do Ucb1 — comparável diretamente).
pub fn arrependimento_empirico(&self, recompensas_verdadeiras: &[f64]) -> f64 {
let melhor = recompensas_verdadeiras
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max);
self.jogadas
.iter()
.zip(recompensas_verdadeiras.iter())
.map(|(&c, &p)| c as f64 * (melhor - p))
.sum()
}
}
// ---------------------------------------------------------------------------
// v0.7.0 — Auto-boost limitado dos SOMs + detecção de stop-learning
// (herdado da análise de BiGRU_T_version/som_auto_adjust.py — r-28 §2.2)
// ---------------------------------------------------------------------------
/// Auto-ajuste dos SOMs pelas métricas QE/TE com **tetos formais**:
/// α₀_ef ∈ [α₀, 2α₀] e σ₀_ef ∈ [σ₀, 2σ₀] — nunca abaixo do canônico
/// (piso FIX-3), no máximo 2× (evita divergência).
///
/// **Teo S-AB (boost limitado):** o mapa de atualização é SEM ESTADO e
/// limitado: α_eff = clamp(α₀·(1+0,3·qe)·(1−0,2·cross), α₀, 2α₀) — por
/// construção |α_eff − α₀| ≤ α₀ para QUALQUER entrada qe, cross ∈ [0,1]²
/// ⇒ o treinamento permanece no envelope de estabilidade do canônico
/// (recorrência limitada monotônica não diverge). ∎ (r-33 §2)
///
/// **Teo S-SL (stop-learning conservador):** a decisão de parar é função
/// da filtração observada (QE/TE/mortos/NaN por chunk) com 5 gatilhos
/// herdados e catalogados; parar cedo só REDUZ o custo (nunca degrada o
/// estado já aprendido — os pesos não são tocados pela decisão). ∎
pub struct AutoAjusteSom {
pub alfa0: f64,
pub sigma0: f64,
/// contadores de chunks consecutivos (stop-learning).
pub chunks_qe_critico: usize,
pub chunks_crossing_severo: usize,
pub chunks_nan: usize,
pub parado: bool,
pub motivo_parada: String,
}
impl AutoAjusteSom {
pub fn novo(alfa0: f64, sigma0: f64) -> Self {
AutoAjusteSom {
alfa0,
sigma0,
chunks_qe_critico: 0,
chunks_crossing_severo: 0,
chunks_nan: 0,
parado: false,
motivo_parada: String::new(),
}
}
/// Passo de auto-ajuste: devolve (α_eff, σ_eff) DENTRO dos tetos.
/// qe_norm/te_norm ∈ [0,1] (normalizados); cross ∈ [0,1] severidade.
pub fn passo(&mut self, qe_norm: f64, te_norm: f64, cross: f64) -> (f64, f64) {
let alfa = (self.alfa0 * (1.0 + 0.3 * qe_norm) * (1.0 - 0.2 * cross))
.clamp(self.alfa0, 2.0 * self.alfa0);
let sigma = (self.sigma0 * (1.0 + 0.4 * te_norm) + 0.25 * cross)
.clamp(self.sigma0, 2.0 * self.sigma0);
(alfa, sigma)
}
/// Stop-learning (5 gatilhos — BiGRU V6.5-V2 + V7, adaptados):
/// 1. colapso total com revival falhou (colapso=true, revival=false)
/// 2. QE estagnado em nível crítico por ≥ 5 chunks consecutivos
/// 3. neurônios mortos > 0,8 após revival
/// 4. todas as métricas NaN por 2 chunks
/// 5. crossing severo por ≥ 4 chunks consecutivos
pub fn avaliar_parada(
&mut self,
qe_norm: f64,
te_norm: f64,
fracao_mortos: f64,
colapso: bool,
revival_ok: bool,
nan_tudo: bool,
crossing_severo: bool,
) -> bool {
if self.parado {
return true;
}
// gatilho 2
self.chunks_qe_critico = if qe_norm > 0.9 { self.chunks_qe_critico + 1 } else { 0 };
// gatilho 5
self.chunks_crossing_severo = if crossing_severo {
self.chunks_crossing_severo + 1
} else {
0
};
// gatilho 4
self.chunks_nan = if nan_tudo { self.chunks_nan + 1 } else { 0 };
let _ = te_norm;
let motivo = if colapso && !revival_ok {
Some("colapso topológico com revival falhou")
} else if self.chunks_qe_critico >= 5 {
Some("QE estagnado em nível crítico por 5 chunks")
} else if fracao_mortos > 0.8 {
Some("neurônios mortos > 80% após revival")
} else if self.chunks_nan >= 2 {
Some("todas as métricas NaN por 2 chunks")
} else if self.chunks_crossing_severo >= 4 {
Some("crossing severo por 4 chunks (instabilidade crônica)")
} else {
None
};
if let Some(m) = motivo {
self.parado = true;
self.motivo_parada = m.to_string();
return true;
}
false
}
}
#[cfg(test)]
mod testes_v070 {
use super::*;
/// S-AB: tetos respeitados para entradas EXTREMAS.
#[test]
fn sab_tetos_dos_boosts() {
let mut a = AutoAjusteSom::novo(0.5, 1.0);
for (qe, te, cross) in [
(0.0f64, 0.0, 0.0),
(1.0, 1.0, 1.0),
(1.0, 0.0, 1.0),
(0.5, 0.5, 0.5),
] {
let (al, sg) = a.passo(qe, te, cross);
assert!(al >= 0.5 && al <= 1.0, "α_eff {al} ∈ [α₀, 2α₀]");
assert!(sg >= 1.0 && sg <= 2.0, "σ_eff {sg} ∈ [σ₀, 2σ₀]");
}
// QE alta sem crossing ⇒ α sobe
let (al, _) = a.passo(1.0, 0.0, 0.0);
assert!((al - 0.65).abs() < 1e-9, "α = α₀·1,3 = {al}");
// crossing puro ⇒ α recua ao piso mas nunca abaixo
let (al, _) = a.passo(0.0, 0.0, 1.0);
assert!((al - 0.5).abs() < 1e-9, "α no piso {al}");
}
/// S-SL: gatilhos disparam; pesos não são tocados pela decisão.
#[test]
fn ssl_stop_learning() {
let mut a = AutoAjusteSom::novo(0.5, 1.0);
// 5 chunks consecutivos de QE crítico ⇒ para
for _ in 0..4 {
assert!(!a.avaliar_parada(0.95, 0.1, 0.1, false, true, false, false));
}
assert!(a.avaliar_parada(0.95, 0.1, 0.1, false, true, false, false));
assert!(a.parado);
assert!(a.motivo_parada.contains("QE estagnado"));
// parado persiste
assert!(a.avaliar_parada(0.1, 0.1, 0.1, false, true, false, false));
// colapso imediato
let mut b = AutoAjusteSom::novo(0.5, 1.0);
assert!(b.avaliar_parada(0.1, 0.1, 0.1, true, false, false, false));
// NaN 2 chunks
let mut c = AutoAjusteSom::novo(0.5, 1.0);
assert!(!c.avaliar_parada(0.1, 0.1, 0.1, false, true, true, false));
assert!(c.avaliar_parada(0.1, 0.1, 0.1, false, true, true, false));
// reset do contador com QE bom
let mut d = AutoAjusteSom::novo(0.5, 1.0);
for _ in 0..3 {
d.avaliar_parada(0.95, 0.1, 0.1, false, true, false, false);
}
d.avaliar_parada(0.2, 0.1, 0.1, false, true, false, false); // reseta
assert_eq!(d.chunks_qe_critico, 0);
assert!(!d.parado);
}
}
#[cfg(test)]
mod testes_v060 {
use super::*;
#[test]
fn a1_tau_c_quantil() {
let mut hub = TelemetryHub::novo(None);
let mut aa = AutoAjustePensamento::novo(0.5, 4, 4);
// confianças uniformes [0,1): quantil 0.8 ≈ 0.8
let mut rng = crate::rng::RngDeterministico::novo(3);
for _ in 0..200 {
aa.confs_retidas.push(rng.uniforme());
}
let tau = aa.ajustar_tau_c(0.20, &mut hub);
assert!((tau - 0.8).abs() < 0.06, "quantil ~0.8 (τ={tau:.3})");
assert!(!hub.eventos_ajuste.is_empty(), "ajuste auditado");
}
#[test]
fn a3_lema_do_acordo() {
let mut hub = TelemetryHub::novo(None);
let mut aa = AutoAjustePensamento::novo(0.5, 4, 2);
// acordo alto (0.9) ⇒ p̂ ≈ (1+√0.8)/2 ≈ 0.947 ⇒ margem grande ⇒ N pequeno
for _ in 0..32 {
aa.acordos.push(0.9);
}
let n1 = aa.ajustar_n(0.05, 16, &mut hub);
// p̂ = (1+√0.8)/2 ≈ 0.947 ⇒ N ≥ ln(20)/(2·0.447²) ≈ 7.5 ⇒ 8
assert!(n1 <= 8, "acordo alto ⇒ N pequeno (N={n1})");
// acordo ~0.5 ⇒ p̂ = 0.5 ⇒ N = máximo
let mut aa2 = AutoAjustePensamento::novo(0.5, 4, 2);
for _ in 0..32 {
aa2.acordos.push(0.5);
}
let n2 = aa2.ajustar_n(0.05, 16, &mut hub);
assert_eq!(n2, 16, "p̂=1/2 ⇒ Hoeffding exige N = teto");
}
#[test]
fn a5_gate_somente_com_lcb_positivo() {
let mut hub = TelemetryHub::novo(None);
let mut aa = AutoAjustePensamento::novo(0.5, 4, 4);
// ganhos estáveis positivos (n=64: 3º termo de Bernstein = 7·ln(40)/189 ≈ 0.14
// ⇒ LCB = 0.30 − 0.14 > 0) ⇒ gate sobe
for _ in 0..64 {
aa.ajustar_gate(0.30, 0.05, &mut hub);
}
assert!(aa.gate_g > 0.0, "LCB positivo ⇒ gate sobe (g={})", aa.gate_g);
// ganhos negativos ⇒ gate recua
let mut aa2 = AutoAjustePensamento::novo(0.5, 4, 4);
aa2.gate_g = 0.5;
for _ in 0..64 {
aa2.ajustar_gate(-0.30, 0.05, &mut hub);
}
assert!(aa2.gate_g < 0.5, "LCB negativo ⇒ gate recua (g={})", aa2.gate_g);
}
#[test]
fn a4_ucb1_converge_para_o_melhor_braco() {
let mut u = Ucb1::novo(vec!["a".into(), "b".into(), "c".into()]);
let mut rng = crate::rng::RngDeterministico::novo(9);
// braço 1 é o melhor (recompensa média 0.9 vs 0.3/0.5)
for _ in 0..300 {
let braco = u.escolher();
let media = match braco { 0 => 0.3, 1 => 0.9, _ => 0.5 };
let r = (media + rng.normal(0.0, 0.05) as f64).clamp(0.0, 1.0);
u.registrar(braco, r);
}
assert_eq!(u.contagens.as_slice().position_max().unwrap(), 1, "UCB1 encontra o melhor braço");
assert!(u.arrependimento_empirico() / 300.0 < 0.15, "arrependimento médio pequeno");
}
#[test]
fn a2_orcamento_marginal() {
let mut hub = TelemetryHub::novo(None);
let mut aa = AutoAjustePensamento::novo(0.5, 8, 4);
// ganhos decaindo geometricamente ρ ≈ 0.5
for k in 0..12 {
aa.ganhos_por_iteracao.push(0.2 * 0.5f64.powi(k));
}
let b = aa.ajustar_orcamento(0.01, &mut hub);
assert!(b >= 1 && b <= 16, "B marginal finito (B={b})");
}
}
/// Extensão: posição do máximo (estável para testes).
trait PosMax {
fn position_max(&self) -> Option<usize>;
}
impl PosMax for [u64] {
fn position_max(&self) -> Option<usize> {
self.iter().enumerate().max_by_key(|(_, v)| **v).map(|(i, _)| i)
}
}
#[cfg(test)]
mod testes_v090 {
use super::*;
/// T-TS2 (falsificação do arrependimento): bandit sintético de 3 braços
/// com probabilidades conhecidas (0,9 / 0,6 / 0,3) — Thompson e UCB1
/// devem AMBOS concentrar jogadas no melhor braço e ter arrependimento
/// sublinear (bem abaixo do uniforme); Thompson com constante empírica
/// não-pior que 2× a do UCB1 (gate direcional, não teórico).
#[test]
fn ts_vs_ucb1_arrependimento() {
let ps = [0.9f64, 0.6, 0.3];
let melhor = 0usize;
let t_max = 4000u32;
let mut ts = ThompsonSampling::novo(
ps.iter().map(|p| format!("braço-{p}")).collect(),
101,
);
let mut u = Ucb1::novo(ps.iter().map(|p| format!("braço-{p}")).collect());
let mut rng = crate::rng::RngDeterministico::novo(202);
let (mut soma_ts, mut soma_u, mut soma_unif) = (0.0f64, 0.0f64, 0.0f64);
let (mut jogadas_ts_melhor, mut jogadas_u_melhor) = (0u32, 0u32);
for t in 0..t_max {
// Thompson
let k = ts.escolher();
let sucesso = rng.uniforme() < ps[k];
ts.registrar(k, sucesso);
soma_ts += (ps[melhor] - ps[k]) as f64;
if k == melhor {
jogadas_ts_melhor += 1;
}
// UCB1
let k2 = u.escolher();
let sucesso2 = rng.uniforme() < ps[k2];
u.registrar(k2, if sucesso2 { 1.0 } else { 0.0 });
soma_u += (ps[melhor] - ps[k2]) as f64;
if k2 == melhor {
jogadas_u_melhor += 1;
}
// uniforme (referência de nenhuma aprendizagem)
let k3 = t as usize % ps.len();
soma_unif += (ps[melhor] - ps[k3]) as f64;
}
// ambos aprendem: arrependimento médio despenca vs uniforme
let (r_ts, r_u, r_unif) = (soma_ts / t_max as f64, soma_u / t_max as f64, soma_unif / t_max as f64);
assert!(
r_ts < 0.5 * r_unif && r_u < 0.5 * r_unif,
"TS {r_ts} / UCB1 {r_u} devem aprender vs uniforme {r_unif}"
);
// gate direcional de eficiência: TS na mesma ordem de grandeza do UCB1
assert!(
r_ts < 2.5 * r_u.max(1e-9) + 0.02,
"TS {r_ts} indignamente pior que UCB1 {r_u}"
);
// o melhor braço domina as jogadas em ambos
assert!(
jogadas_ts_melhor as f64 > 0.6 * t_max as f64,
"TS jogou o melhor braço só {jogadas_ts_melhor}/{t_max}"
);
assert!(
jogadas_u_melhor as f64 > 0.6 * t_max as f64,
"UCB1 jogou o melhor braço só {jogadas_u_melhor}/{t_max}"
);
// posterior de TS aproxima as probabilidades verdadeiras
let medias = ts.medias_posteriori();
for (i, &p) in ps.iter().enumerate() {
assert!((medias[i] - p).abs() < 0.15, "posterior[{i}]={:.3} ≠ {p}", medias[i]);
}
}
/// Determinismo bit-a-bit da trajetória do Thompson (mesma semente).
#[test]
fn ts_deterministico() {
let ps = [0.8f64, 0.4];
let traj = |semente: u64| -> Vec<usize> {
let mut ts = ThompsonSampling::novo(vec!["a".into(), "b".into()], semente);
let mut rng = crate::rng::RngDeterministico::novo(semente + 1);
(0..500)
.map(|_| {
let k = ts.escolher();
ts.registrar(k, rng.uniforme() < ps[k]);
k
})
.collect()
};
assert_eq!(traj(7), traj(7), "mesma semente ⇒ mesma trajetória");
assert_ne!(traj(7), traj(8), "sementes diferentes divergem");
}
/// Bernoulli-ização de recompensas contínuas: posterior converge para
/// a média da recompensa (T-TS1.4 — a probabilidade de sucesso É a
/// própria recompensa em expectativa).
#[test]
fn ts_recompensa_continua() {
let mut ts = ThompsonSampling::novo(vec!["só".into()], 55);
for i in 0..20_000 {
// recompensa determinística 0,25 ⇒ posterior → 0,25
ts.registrar_continuo(0, 0.25);
let _ = i;
}
let m = ts.medias_posteriori()[0];
assert!((m - 0.25).abs() < 0.02, "posterior {m} ≠ 0,25");
}
}