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 Download src/controlador.rs from PowerMachine/khtst-rust: direct link, hf CLI and curl.
- Browser
- Download file 34 kB
-
https://huggingface.co/PowerMachine/khtst-rust/resolve/main/src/controlador.rs
- Command line
-
hf download hf://PowerMachine/khtst-rust/src/controlador.rs
-
curl -L -o controlador.rs https://huggingface.co/PowerMachine/khtst-rust/resolve/main/src/controlador.rs
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 | |
| } | |
| } | |
| mod testes_v070 { | |
| use super::*; | |
| /// S-AB: tetos respeitados para entradas EXTREMAS. | |
| 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. | |
| 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); | |
| } | |
| } | |
| mod testes_v060 { | |
| use super::*; | |
| 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"); | |
| } | |
| 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"); | |
| } | |
| 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); | |
| } | |
| 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"); | |
| } | |
| 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) | |
| } | |
| } | |
| 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). | |
| 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). | |
| 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). | |
| 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"); | |
| } | |
| } | |