//! Loops de treino (fases A e B — n-04 §4) com otimizador AdamW MANUAL //! (controle total para PCGrad multi-perda, clip global e LR dinâmico do //! controlador), telemetria completa, callback Python periódico e //! checkpoints atômicos (Teo 35.3). use candle_core::{Device, Result, Tensor, Var}; use candle_nn::loss::cross_entropy; use rand::prelude::*; use std::collections::HashMap; use crate::config::ConfigKhtst; use crate::controlador::Controlador; use crate::gerador::GeradorConceitos; use crate::memoria::{JepaLatente, SomConceitos}; use crate::mascaras::interp_para; use crate::telemetria::TelemetryHub; use crate::tokenizador::TokenizadorConceitos; /// AdamW manual (estado por índice de variável; grads achatados). pub struct AdamWManual { pub m: HashMap>, pub v: HashMap>, pub t: usize, pub lr: f64, pub beta1: f64, pub beta2: f64, pub eps: f64, pub wd: f64, pub vars: Vec, } impl AdamWManual { pub fn novo(vars: Vec, lr: f64, wd: f64) -> Self { AdamWManual { m: HashMap::new(), v: HashMap::new(), t: 0, lr, beta1: 0.9, beta2: 0.999, eps: 1e-8, wd, vars } } pub fn set_lr(&mut self, lr: f64) { self.lr = lr; } /// Um passo. `grads`: por variável, gradiente ACHATADO (None = zero). /// /// v0.9.0: o loop interno é o kernel fundido `simd::passo_adamw` (AVX-512 /// com despacho em runtime + fallback escalar) — réplica EXATA da ordem /// de operações do loop escalar da v0.8.0 (incluindo `(1.0 - beta as f32)` /// pós-cast), sem FMA/rsqrt ⇒ saída bit-a-bit idêntica (Teo X-1, r-34 §2; /// teste `adamw_bit_a_bit`). pub fn passo(&mut self, grads: Vec>>) { self.t += 1; let bc1 = 1.0 - self.beta1.powi(self.t as i32); let bc2 = 1.0 - self.beta2.powi(self.t as i32); for (i, var) in self.vars.iter().enumerate() { let Some(g) = grads.get(i).cloned().flatten() else { continue }; let n = g.len(); let m = self.m.entry(i).or_insert(vec![0f32; n]); let v = self.v.entry(i).or_insert(vec![0f32; n]); let dados = var.as_tensor().flatten_all().unwrap().to_vec1::().unwrap(); let mut novos = vec![0f32; n]; crate::simd::passo_adamw( &dados, &g, m, v, &mut novos, self.lr, self.beta1, self.beta2, self.eps, self.wd, bc1, bc2, ); let forma = var.as_tensor().shape(); let dev = var.as_tensor().device(); let novo_t = Tensor::from_vec(novos, forma.clone(), dev).unwrap() .reshape(forma).unwrap(); let _ = var.set(&novo_t); } } /// Norma global de gradiente (para clip e telemetria). pub fn norma_global(grads: &[Option>]) -> f64 { let mut s = 0f64; for g in grads.iter().flatten() { for x in g { s += (*x as f64) * (*x as f64); } } s.sqrt() } /// Clip global por norma (in-place nos grads achatados). pub fn clipar(grads: &mut [Option>], teto: f64) { let norma = Self::norma_global(grads); if !norma.is_finite() { // gradiente explodido (inf/nan): zera o passo — seguro por // convergir para o ponto anterior (sem divergir) for g in grads.iter_mut().flatten() { for x in g.iter_mut() { *x = 0.0; } } return; } if norma > teto && norma > 0.0 { let f = (teto / norma) as f32; for g in grads.iter_mut().flatten() { // v0.9.0: kernel SIMD bit-a-bit (uma mul por elemento) crate::simd::escalar_slice(g, f); } } } } /// Extrai grads achatados do GradStore na ordem de `vars`. pub fn grads_achatados(perda: &Tensor, vars: &[Var]) -> Result>>> { let gs = perda.backward()?; let mut out = Vec::with_capacity(vars.len()); for v in vars { match gs.get(v) { Some(t) => out.push(Some(t.flatten_all()?.to_vec1::()?)), None => out.push(None), } } Ok(out) } /// PCGrad multi-perda sobre grads achatados (Teo D.7 — projeção por pares). /// v070-3 (auditoria): projeta contra os gradientes ORIGINAIS (snapshot) — /// a v0.6.0 usava grads[j] já modificados quando j < i, violando a não-piora /// pareada ⟨g_i^pc, g_i⟩ ≥ 0 (Yu et al. 2020, Algoritmo 1 usa os originais). pub fn pcgrad_achatado(grads: Vec>>>) -> Vec>> { let n_perdas = grads.len(); let n_vars = grads[0].len(); let originais = grads.clone(); // snapshot imutável dos originais let mut grads = grads; let mut saida: Vec>> = vec![None; n_vars]; for i in 0..n_perdas { // projeta g_i contra cada g_j CONFLITANTE ORIGINAL for j in 0..n_perdas { if i == j { continue; } let mut ip = 0f64; let mut nj = 0f64; for v in 0..n_vars { if let (Some(a), Some(b)) = (&grads[i][v], &originais[j][v]) { for k in 0..a.len().min(b.len()) { ip += a[k] as f64 * b[k] as f64; nj += b[k] as f64 * b[k] as f64; } } } if ip < 0.0 && nj > 1e-12 { let alfa = (ip / nj) as f32; for v in 0..n_vars { if let Some(bv) = &originais[j][v] { if let Some(a) = &mut grads[i][v] { for k in 0..a.len().min(bv.len()) { a[k] -= alfa * bv[k]; } } } } } } for v in 0..n_vars { if let Some(a) = &grads[i][v] { let media: Vec = a.iter().map(|x| x / n_perdas as f32).collect(); saida[v] = Some(match &saida[v] { Some(s) => s.iter().zip(media.iter()).map(|(x, y)| x + y).collect(), None => media, }); } } } saida } pub struct DadosTreino { pub sequencias: Vec>, // cada uma de comprimento N (= ctx) } impl DadosTreino { pub fn novo(sequencias: Vec>, n: usize) -> Self { let seqs = sequencias .into_iter() .filter(|s| s.len() >= 8) .map(|mut s| { while s.len() < n { s.push(crate::tokens::PAD); } s.truncate(n); s }) .collect(); DadosTreino { sequencias: seqs } } pub fn lote(&self, b: usize, rng: &mut StdRng, dev: &Device) -> Result { let mut escolhidas = Vec::with_capacity(b); for _ in 0..b { if self.sequencias.is_empty() { break; } escolhidas.push(self.sequencias[rng.gen_range(0..self.sequencias.len())].clone()); } let b_efetivo = escolhidas.len().max(1); let n = escolhidas[0].len(); let plano: Vec = escolhidas.into_iter().flatten().collect(); Tensor::from_vec(plano, (b_efetivo, n), dev) } } /// Callback Python opcional (chamado a cada `a_cada` passos com um dict). pub type CallbackPy = Option>; pub fn chamar_callback(cb: &CallbackPy, dados: &serde_json::Value) { if let Some(f) = cb { pyo3::Python::with_gil(|py| { if let Ok(obj) = pythonize::pythonize(py, dados) { let _ = f.call1(py, (obj,)); } }); } } pub struct ResumoFase { pub passos: usize, pub perda_final: f64, pub perda_inicial: f64, pub acc_recon: f64, pub delta_ort_max: f64, pub perplexidade_codebook: f64, pub monotonia_prefixo: f64, pub escore_aprendizado: f64, } /// FASE A — tokenizador (n-04 §4): nested dropout + commitment + GSQ + RCO. pub fn treinar_fase_a( cfg: &ConfigKhtst, dados: &DadosTreino, hub: &mut TelemetryHub, callback: &CallbackPy, caminho_checkpoint: Option<&str>, teto_seg: Option, ) -> Result { let dev = Device::Cpu; let inicio = std::time::Instant::now(); let mut tok = TokenizadorConceitos::novo(cfg, &dev)?; let vars = tok.vars(); let mut opt = AdamWManual::novo(vars.clone(), cfg.treino.lr, cfg.treino.peso_decaimento); let mut ctrl = Controlador::novo(cfg.treino.lr, cfg.treino.aquecimento, cfg.treino.passos_fase_a, cfg.telemetria.paciencia_plateau, cfg.telemetria.fator_lr_plateau, cfg.ncm.lambdas.rho_ancora); let mut rng = StdRng::seed_from_u64(31_7); // RNG dedicado (Teo 31.7) let perda_inicial = { let ids = dados.lote(cfg.treino.lote, &mut rng, &dev)?; let (p, _) = tok.perda_fase_a(&ids, &mut rng)?; p.to_scalar::()? as f64 }; let a_cada = cfg.treino.telemetria_a_cada.max(1); let mut passos_feitos = 0usize; 'laco_a: for passo in 0..cfg.treino.passos_fase_a { hub.passo_corrente = passo; if let Some(t) = teto_seg { if inicio.elapsed().as_secs_f64() > t { passos_feitos = passo; break 'laco_a; } } let ids = dados.lote(cfg.treino.lote, &mut rng, &dev)?; let (perda_t, _m) = tok.perda_fase_a(&ids, &mut rng)?; let perda = perda_t.to_scalar::()? as f64; // agenda τ (Teo 30.6) tok.agendar_tau(passo, cfg.ncm.gsq.tau0, cfg.ncm.gsq.tau_min, cfg.ncm.gsq.t_alvo); // LR + clip do controlador (ajustes AUTOMÁTICOS — auditados) let lr = ctrl.lr_passo(passo, perda, hub); opt.set_lr(lr); let mut grads = grads_achatados(&perda_t, &vars)?; let norma = AdamWManual::norma_global(&grads); ctrl.registrar_norma(norma); let teto = ctrl.clip_corrente(hub); AdamWManual::clipar(&mut grads, teto); opt.passo(grads); // EMA do codebook (modo ste_ema) if cfg.ncm.modo_quantizacao == "ste_ema" { let cod = tok.codificar_congelado(&ids)?; tok.atualizar_ema_codebook(&cod.ids)?; } // telemetria hub.registrar_multi(&[ ("perda", perda), ("lr", lr), ("norma_grad", norma), ]); if passo % a_cada == 0 || passo == cfg.treino.passos_fase_a - 1 { let perp: f64 = tok.quants.iter().map(|q| q.perplexidade_uso()).sum::() / tok.quants.len().max(1) as f64; let dort: f64 = tok.quants.iter().map(|q| q.delta_ort_ultimo).fold(0f64, f64::max); hub.registrar_multi(&[ ("codebook_perplexidade_media", perp), ("delta_ort_max", dort), ("escore_aprendizado", hub.escore_aprendizado()), ]); if cfg.telemetria.deteccao_aprendizado { let snap = serde_json::json!({ "fase": "A", "passo": passo, "perda": perda, "lr": lr, "codebook_perplexidade_media": perp, "delta_ort_max": dort, "escore": hub.escore_aprendizado(), }); chamar_callback(callback, &snap); } } if !perda.is_finite() { hub.erro("fase_a", &format!("perda não finita no passo {passo}")); break; } } if passos_feitos == 0 { passos_feitos = cfg.treino.passos_fase_a; } // avaliação final: monotonia de prefixo (V3) + perplexidade let ids_val = dados.lote(cfg.treino.lote, &mut rng, &dev)?; let ces_prefixo = tok.ce_por_prefixo(&ids_val)?; let mut pares_ok = 0usize; let mut pares = 0usize; for w in ces_prefixo.windows(2) { pares += 1; if w[1] <= w[0] * 1.02 { pares_ok += 1; } } let monotonia = pares_ok as f64 / pares.max(1) as f64; let perda_final = ces_prefixo.last().copied().unwrap_or(f64::NAN); let perp: f64 = tok.quants.iter().map(|q| q.perplexidade_uso()).sum::() / tok.quants.len().max(1) as f64; let dort = tok.quants.iter().map(|q| q.delta_ort_ultimo).fold(0f64, f64::max); let acc = { let (_, m) = tok.perda_fase_a(&ids_val, &mut rng)?; m.acc_recon }; if let Some(c) = caminho_checkpoint { tok.salvar(c)?; } hub.registrar("passos_feitos", passos_feitos as f64); hub.dump_jsonl(); Ok(ResumoFase { passos: passos_feitos, perda_final, perda_inicial, acc_recon: acc, delta_ort_max: dort, perplexidade_codebook: perp, monotonia_prefixo: monotonia, escore_aprendizado: hub.escore_aprendizado(), }) } /// FASE B — gerador (n-04 §4): MLE exato sobre hierarquias congeladas (B.1), /// corrupção κ + dropout CFG (C.3/D.2), MTP auxiliar (D.6), JEPA latente e /// PCGrad entre as perdas (D.7). pub fn treinar_fase_b( cfg: &ConfigKhtst, tok: &TokenizadorConceitos, dados: &DadosTreino, hub: &mut TelemetryHub, callback: &CallbackPy, caminho_checkpoint: Option<&str>, teto_seg: Option, ) -> Result { let dev = Device::Cpu; let inicio = std::time::Instant::now(); let mut ger = GeradorConceitos::novo(cfg, &dev)?; let jepa = JepaLatente::novo(cfg.ncm.d_lat, &dev)?; let mut som = SomConceitos::novo(cfg.memoria.som_lado, cfg.ncm.d_lat, &dev)?; let mut vars = ger.vars(); vars.extend(jepa.vars()); let mut opt = AdamWManual::novo(vars.clone(), cfg.treino.lr * 0.7, cfg.treino.peso_decaimento); let mut ctrl = Controlador::novo(cfg.treino.lr * 0.7, cfg.treino.aquecimento, cfg.treino.passos_fase_b, cfg.telemetria.paciencia_plateau, cfg.telemetria.fator_lr_plateau, cfg.ncm.lambdas.rho_ancora); let mut rng = StdRng::seed_from_u64(31_7); let a_cada = cfg.treino.telemetria_a_cada.max(1); let lambda_jepa = cfg.memoria.lambda_jepa; let perda_inicial = { let ids = dados.lote(cfg.treino.lote, &mut rng, &dev)?; let cod = tok.codificar_congelado(&ids)?; let s = ger.perda_treino(tok, &cod.ids, &mut rng)?; s.ce_por_nivel.iter().sum::() / s.ce_por_nivel.len() as f64 }; let mut passos_feitos = 0usize; 'laco_b: for passo in 0..cfg.treino.passos_fase_b { hub.passo_corrente = passo; if let Some(t) = teto_seg { if inicio.elapsed().as_secs_f64() > t { passos_feitos = passo; break 'laco_b; } } let ids = dados.lote(cfg.treino.lote, &mut rng, &dev)?; let cod = tok.codificar_congelado(&ids)?; let saida = ger.perda_treino(tok, &cod.ids, &mut rng)?; let ce = saida.ce_por_nivel.iter().sum::() / saida.ce_por_nivel.len() as f64; // JEPA latente (alvo com stop-grad — contrato doc 30) let perda_jepa_t = { let b = ids.dim(0)?; let zhats = tok.reconstruir_de_codigos(&cod.ids, cod.ids.len(), b, &dev)?; let mut soma = None; for l in 1..zhats.len() { let n_l = zhats[l].dim(1)?; let ant = interp_para(&zhats[l - 1], n_l, &dev)?; let alvo = (&zhats[l] - &ant)?.detach(); let pj = jepa.perda(&ant, &alvo)?; soma = Some(match soma { Some(s) => (&s + &pj)?, None => pj }); } match soma { Some(s) => s.affine(lambda_jepa / zhats.len().max(1) as f64, 0.0)?, None => Tensor::zeros((), candle_core::DType::F32, &dev)?, } }; // perdas separadas para PCGrad: [CE (com MTP embutido), JEPA] let mut grads_lista = Vec::new(); grads_lista.push(grads_achatados(&saida.perda, &vars)?); grads_lista.push(grads_achatados(&perda_jepa_t, &vars)?); let mut combinado = pcgrad_achatado(grads_lista); let norma = AdamWManual::norma_global(&combinado); ctrl.registrar_norma(norma); let lr = ctrl.lr_passo(passo, ce, hub); opt.set_lr(lr); let teto = ctrl.clip_corrente(hub); AdamWManual::clipar(&mut combinado, teto); opt.passo(combinado); // SOM pós-geração (stop-grad — Teo 17.4): insere vetores de nível 1 if passo % a_cada == 0 { let b = ids.dim(0)?; let zhats = tok.reconstruir_de_codigos(&cod.ids, 1, b, &dev)?; let _eq = som.inserir(&zhats[0])?; hub.registrar("som_eq_medio", som.eq_medio); } hub.registrar_multi(&[ ("perda", ce), ("ce_mtp", saida.ce_mtp), ("acc_conceitos", saida.acuracia_media), ("lr", lr), ("norma_grad", norma), ]); if passo % a_cada == 0 || passo == cfg.treino.passos_fase_b - 1 { hub.registrar("escore_aprendizado", hub.escore_aprendizado()); let snap = serde_json::json!({ "fase": "B", "passo": passo, "perda": ce, "ce_mtp": saida.ce_mtp, "acc_conceitos": saida.acuracia_media, "lr": lr, "escore": hub.escore_aprendizado(), }); chamar_callback(callback, &snap); } if !ce.is_finite() { hub.erro("fase_b", &format!("CE não finita no passo {passo}")); break; } } if passos_feitos == 0 { passos_feitos = cfg.treino.passos_fase_b; } let ids_val = dados.lote(cfg.treino.lote, &mut rng, &dev)?; let cod = tok.codificar_congelado(&ids_val)?; let saida = ger.perda_treino(tok, &cod.ids, &mut rng)?; let perda_final = saida.ce_por_nivel.iter().sum::() / saida.ce_por_nivel.len() as f64; if let Some(c) = caminho_checkpoint { ger.salvar(c)?; } hub.dump_jsonl(); Ok(ResumoFase { passos: cfg.treino.passos_fase_b, perda_final, perda_inicial, acc_recon: saida.acuracia_media, delta_ort_max: 0.0, perplexidade_codebook: 0.0, monotonia_prefixo: 0.0, escore_aprendizado: hub.escore_aprendizado(), }) } // --------------------------------------------------------------------------- // v0.6.0 — FASE C: treino conjunto Pensamento↔backbone (r-24) // --------------------------------------------------------------------------- pub struct ResumoFaseC { pub passos: usize, pub ce_inicial: f64, pub ce_final: f64, /// CE média nas sequências GATILHADAS (conf < τ_c) SEM correção. pub ce_gatilhada_sem_correcao: f64, /// CE média nas sequências gatilhadas COM a correção do Pensamento /// (mesma corrupção κ — comparação controlada). pub ce_gatilhada_com_correcao: f64, pub confianca_media_inicial: f64, pub confianca_media_final: f64, pub confianca_pos_correcao: f64, pub taxa_gatilho: f64, pub ece_antes: f64, pub ece_depois: f64, pub temperatura_calibrada: f64, pub tau_c: f64, pub taxa_conflito_pcgrad: f64, pub decaimento_delta: f64, pub acordos_medios: f64, pub escore_aprendizado: f64, pub rodadas_acoplador: u64, pub timeouts_acoplador: u64, pub maior_sobreposicao_us: u128, } /// **FASE C — ajuste fino conjunto Pensamento↔backbone** (fecha os débitos /// r-21 §3.1/§3.3): o verificador é a CONFIANÇA CALIBRADA do próprio NCM /// (sem oráculo); o gradiente flui NOS DOIS SENTIDOS (correção → backbone /// via condicionamento corrigido; backbone → correção via resumo `a` do /// tronco no condicionamento das tentativas). /// /// Protocolo por passo (r-24 §2): /// 1. lote → códigos congelados (fase A) → UMA corrupção κ (mesma nos /// passos 1 e 2 — comparação controlada sem/com correção); /// 2. PASSO-1: forward do gerador → confiança calibrada por sequência → /// gatilho `1{conf < τ_c}` (T-P1) e CE "sem correção" nas gatilhadas; /// 3. cond = [a; b; c]: a = resumo do tronco (destacado), b = ẑ corrompido, /// c = sonda fixa do nível (sem futuro); /// 4. Pensamento em LOTE: N tentativas → agregação diferenciável → δ; /// 5. PASSO-2: ẑ′ = ẑ + δ nas linhas gatilhadas → forward corrigido → /// CE (treina backbone E pensamento — bidirecional) + CE "com correção"; /// 6. [CE_corrigida, L_coop(δ*) + λ_E‖δ‖²] por PCGrad (C-3) — mede /// conflito ⟨g1,g2⟩; /// 7. verificador NCM na predição corrigida (V-2/V-4 — sem oráculo); /// 8. AcopladorDuplo: UMA rodada duplex por passo (estado A→B; correção /// B→A — P.1/P.2/AC-1) + autoajuste A-1..A-5 pela telemetria. pub fn treinar_fase_c( cfg: &ConfigKhtst, tok: &TokenizadorConceitos, dados: &DadosTreino, hub: &mut TelemetryHub, callback: &CallbackPy, teto_seg: Option, passos: usize, caminho_gerador: Option<&str>, ) -> Result { let dev = Device::Cpu; let inicio = std::time::Instant::now(); let mut ger = GeradorConceitos::novo(cfg, &dev)?; // AJUSTE FINO: parte do checkpoint da fase B (quando disponível) if let Some(caminho) = caminho_gerador { ger.carregar(caminho)?; hub.ajuste("fase_c_base", 0.0, 1.0, &format!( "carregado checkpoint da fase B: {caminho} (ajuste fino, não do zero)")); } let d_lat = cfg.ncm.d_lat; let n_tent = 4usize; let d_cond = 3 * d_lat; // a + b + c (T-P6) let nivel_alvo = cfg.ncm.niveis.len() - 1; let n_l_alvo = cfg.ncm.niveis[nivel_alvo]; // motor do pensamento no espaço latente (saída = correção δ) let mut motor = crate::pensamento::MotorPensamento::novo( d_cond, d_lat, n_tent, 0.30, 6, &dev, )?; // sonda fixa do nível-alvo (item c — sem informação do futuro) let mut rng_sonda = crate::rng::RngDeterministico::novo(991); let sonda: Vec = (0..d_lat).map(|_| rng_sonda.normal(0.0, 0.5) as f32).collect(); let sonda_t = Tensor::from_vec(sonda.clone(), (d_lat,), &dev)?; // variáveis: backbone + pensamento (treino CONJUNTO) let mut vars = ger.vars(); vars.extend(motor.vars()); // AJUSTE FINO: LR reduzido (0.15×) — preserva o backbone pré-treinado // (análise marginal: o gradiente conjunto é perturbação de 2ª ordem) let lr_fino = cfg.treino.lr * 0.15; let mut opt = AdamWManual::novo(vars.clone(), lr_fino, cfg.treino.peso_decaimento); let mut ctrl = Controlador::novo( lr_fino, cfg.treino.aquecimento, passos, cfg.telemetria.paciencia_plateau, cfg.telemetria.fator_lr_plateau, cfg.ncm.lambdas.rho_ancora, ); let mut rng = StdRng::seed_from_u64(31_7); // ---- calibração inicial do verificador (dados retidos: 1 lote) ---- let mut verificador = crate::verificador::VerificadorNcm::novo(); let ids_cal = dados.lote(cfg.treino.lote, &mut rng, &dev)?; let cod_cal = tok.codificar_congelado(&ids_cal)?; { let (logits_cal, _, _) = passo_um(&ger, tok, &cod_cal, &mut rng, &dev)?; let b_c = ids_cal.dim(0)?; let plano = logits_cal.reshape((b_c * n_l_alvo, cfg.ncm.k_por_nivel))?; let alvo_plano = cod_cal.ids[nivel_alvo] .reshape((b_c * n_l_alvo,))? .to_dtype(candle_core::DType::U32)?; let (e_antes, e_depois) = verificador.ajustar(&plano, &alvo_plano)?; hub.registrar_multi(&[ ("ece_calibracao", e_antes), ("ece_pos_calibracao", e_depois), ("temperatura_calibrada", verificador.temperatura), ]); // τ_c inicial pelo quantil DKW (A-1) das confianças retidas let confs: Vec = crate::verificador::confianca_por_linha(&plano)? .flatten_all()?.to_vec1::()?.iter().map(|x| *x as f64).collect(); let tau_c = crate::verificador::limiar_por_erro(&confs, 0.30); motor.limiar_conf = tau_c; hub.ajuste("pensamento_tau_c", 0.30, tau_c, "A-1 quantil DKW 1−ε (ε=0.30, calibração inicial fase C)"); } let tau_c0 = motor.limiar_conf; // ---- acoplador duplex + autoajuste ---- let mut acoplador = crate::acoplador::AcopladorDuplo::nova(64, 2000); let mut auto = crate::controlador::AutoAjustePensamento::novo(motor.limiar_conf, 6, n_tent); // ---- medição inicial ---- let (ce_inicial, conf_inicial, _taxa_gatilho_ini) = { let (logits, _, _) = passo_um(&ger, tok, &cod_cal, &mut rng, &dev)?; let plano = logits.reshape((cfg.treino.lote * n_l_alvo, cfg.ncm.k_por_nivel))?; let conf = conf_media(&plano, &verificador)?; let tg = taxa_gatilho(&plano, &verificador, motor.limiar_conf)?; (ce_por_nivel_medio(&ger, tok, &cod_cal, &mut rng)?, conf, tg) }; let a_cada = cfg.treino.telemetria_a_cada.max(1); let mut passos_feitos = 0usize; let mut conflitos = 0usize; let mut pares_grad = 0usize; let mut normas_delta: Vec = Vec::new(); let mut acordos: Vec = Vec::new(); let mut ce_gat_sem_hist: Vec = Vec::new(); let mut ce_gat_com_hist: Vec = Vec::new(); let mut conf_pos_hist: Vec = Vec::new(); let mut taxa_hist: Vec = Vec::new(); let lambda_energia = 0.01f64; 'laco_c: for passo in 0..passos { hub.passo_corrente = passo; if let Some(t) = teto_seg { if inicio.elapsed().as_secs_f64() > t { passos_feitos = passo; break 'laco_c; } } let ids = dados.lote(cfg.treino.lote, &mut rng, &dev)?; let cod = tok.codificar_congelado(&ids)?; let b = ids.dim(0)?; // (1)+(2) PASSO-1: UMA corrupção, forward, gatilho, CE "sem" let (logitos1, resumo_anterior, zhats_corrompidos) = passo_um(&ger, tok, &cod, &mut rng, &dev)?; let plano1 = logitos1.reshape((b * n_l_alvo, ger.k))?; let confs_v: Vec = crate::verificador::confianca_por_linha(&plano1)? .flatten_all()?.to_vec1::()?.iter().map(|x| *x as f64).collect(); let mut conf_seq = vec![0f64; b]; for (i, c) in confs_v.iter().enumerate() { conf_seq[i / n_l_alvo] += c / n_l_alvo as f64; } let gatilho: Vec = conf_seq.iter().map(|c| *c < motor.limiar_conf).collect(); let taxa = gatilho.iter().filter(|g| **g).count() as f64 / b as f64; taxa_hist.push(taxa); // CE "sem correção" nas linhas gatilhadas (desta MESMA corrupção) let ce_gat_sem = ce_nas_linhas(&logitos1, &cod.ids[nivel_alvo], &gatilho)?; if gatilho.iter().any(|g| *g) { // (3) cond = [a; b; c] let a_proj = projeta_para_d_lat(&resumo_anterior, d_lat, &dev)?; let z_ant = zhats_corrompidos[nivel_alvo - 1].clone(); // (b, n_{L-1}, d_lat) let cond = Tensor::cat(&[ a_proj.clone(), z_ant.clone().mean(candle_core::D::Minus2)?, // (b, d_lat) — resumo por linha (T-P6) sonda_t.broadcast_as((b, d_lat))?, ], 1)?; // (4) Pensamento em lote: N tentativas + agregação diferenciável let candidatos = motor.candidatos_lote(&cond, 0.1, 31_7 + passo as u64)?; let (delta, pesos) = motor.agregar_lote(&candidatos)?; // (b, d_lat) let acordo = acordo_tentativas(&candidatos)?; acordos.push(acordo); // (5) PASSO-2: injeção mascarada de δ no condicionamento let n_ant = z_ant.dim(1)?; let mascara: Vec = gatilho.iter().map(|g| if *g { 1.0 } else { 0.0 }).collect(); let m_t = Tensor::from_vec(mascara, (b, 1, 1), &dev)?; let delta_interp = interp_para(&delta.unsqueeze(1)?, n_ant, &dev)?; // (b, n_ant, d_lat) let delta_mascarado = delta_interp.broadcast_mul(&m_t)?; let z_corrigido = (&z_ant + &delta_mascarado)?; // CE "com correção" nas linhas gatilhadas (mesma corrupção) let ce_gat_com = { let logits2 = passo_dois_logits(&ger, tok, &cod, &zhats_corrompidos, &z_corrigido, nivel_alvo)?; ce_nas_linhas(&logits2, &cod.ids[nivel_alvo], &gatilho)? }; // perda CE corrigida (todas as linhas — treina backbone+pensamento) let perda_ce = passo_dois_perda(&ger, tok, &cod, &zhats_corrompidos, &z_corrigido, &mut rng)?; // L_coop: alvo supervisionado δ* = ẑ_verd − ẑ_corr (por linha) let zhats_verd = tok.reconstruir_de_codigos(&cod.ids, nivel_alvo, b, &dev)?; let z_verd = interp_para(&zhats_verd[nivel_alvo - 1], n_ant, &dev)?; let z_corr_b = interp_para(&zhats_corrompidos[nivel_alvo - 1], n_ant, &dev)?; let alvo_delta = (&z_verd - &z_corr_b)?.mean(candle_core::D::Minus2)?; // (b, d_lat) let l_coop = motor.perda_treino(&cond, &alvo_delta)?; let energia = delta_mascarado.sqr()?.sum_all()? .affine(lambda_energia / (b * d_lat) as f64, 0.0)?; let l_coop_total = (&l_coop + &energia)?; // (6) PCGrad [CE corrigida, L_coop] + taxa de conflito let g1 = grads_achatados(&perda_ce, &vars)?; let g2 = grads_achatados(&l_coop_total, &vars)?; { let (ip, ok) = produto_interno_grads(&g1, &g2); if ok { pares_grad += 1; if ip < 0.0 { conflitos += 1; } } } let combinado = pcgrad_achatado(vec![g1, g2]); let norma = AdamWManual::norma_global(&combinado); ctrl.registrar_norma(norma); let lr = ctrl.lr_passo(passo, perda_ce.to_scalar::()? as f64, hub); opt.set_lr(lr); let teto = ctrl.clip_corrente(hub); let mut g = combinado; AdamWManual::clipar(&mut g, teto); opt.passo(g); // (7) verificador NCM na predição CORRIGIDA (sem oráculo) let conf_pos = { let logits2 = passo_dois_logits(&ger, tok, &cod, &zhats_corrompidos, &z_corrigido, nivel_alvo)?; let plano2 = logits2.reshape((b * n_l_alvo, ger.k))?; conf_media(&plano2, &verificador)? }; conf_pos_hist.push(conf_pos); // (8) rodada duplex do acoplador: estado A→B; correção B→A let estado = crate::acoplador::EstadoModelo { a: a_proj.get(0)?.to_vec1::()?, b: z_ant.get(0)?.get(0)?.to_vec1::()?, c: sonda.clone(), confianca: conf_pos, nivel: nivel_alvo, passo, }; acoplador .rodada(estado) .map_err(|e| candle_core::Error::Msg(format!("acoplador: {e}")))?; let delta_vec: Vec = delta.get(0)?.to_vec1::()?; let norma_delta = (delta.sqr()?.sum_all()?.to_scalar::()? as f64).sqrt(); normas_delta.push(norma_delta); acoplador.registrar_correcao(crate::acoplador::Correcao { delta: delta_vec, acordo, confianca_agregada: conf_pos, iteracao: passo, pesos: pesos.clone(), }); // autoajuste A-1..A-5 pela telemetria do passo auto.confs_retidas.extend(confs_v.iter().copied()); auto.acordos.push(acordo); let _ = auto.ajustar_tau_c(0.30, hub); let _ = auto.ajustar_n(0.05, 8, hub); let ganho_conf = conf_pos - conf_seq.iter().sum::() / b as f64; auto.ganhos_por_iteracao.push(ganho_conf.max(0.0)); let _ = auto.ajustar_gate(ganho_conf, 0.05, hub); motor.limiar_conf = auto.tau_c; hub.registrar_multi(&[ ("perda", perda_ce.to_scalar::()? as f64), ("ce_gatilhada_sem", ce_gat_sem), ("ce_gatilhada_com", ce_gat_com), ("confianca_pos_correcao", conf_pos), ("confianca_media", conf_seq.iter().sum::() / b as f64), ("taxa_gatilho", taxa), ("norma_delta", norma_delta), ("acordo_tentativas", acordo), ("lr", lr), ("norma_grad", norma), ]); ce_gat_sem_hist.push(ce_gat_sem); ce_gat_com_hist.push(ce_gat_com); } else { // sem gatilho: backbone puro (aprendizado base continua) let s = ger.perda_treino(tok, &cod.ids, &mut rng)?; let grads = grads_achatados(&s.perda, &vars)?; let norma = AdamWManual::norma_global(&grads); ctrl.registrar_norma(norma); let ce = s.ce_por_nivel.iter().sum::() / s.ce_por_nivel.len() as f64; let lr = ctrl.lr_passo(passo, ce, hub); opt.set_lr(lr); let teto = ctrl.clip_corrente(hub); let mut g = grads; AdamWManual::clipar(&mut g, teto); opt.passo(g); hub.registrar_multi(&[ ("perda", ce), ("taxa_gatilho", 0.0), ("lr", lr), ("norma_grad", norma), ]); } if passo % a_cada == 0 || passo == passos - 1 { hub.registrar("escore_aprendizado", hub.escore_aprendizado()); let snap = serde_json::json!({ "fase": "C", "passo": passo, "escore": hub.escore_aprendizado(), }); chamar_callback(callback, &snap); } let ce_agora = hub.series.get("perda") .and_then(|s| s.last().map(|(_, v)| *v)).unwrap_or(f64::NAN); if !ce_agora.is_finite() { hub.erro("fase_c", &format!("CE não finita no passo {passo}")); break 'laco_c; } } if passos_feitos == 0 { passos_feitos = passos; } // ---- avaliação final NO MESMO LOTE da medição inicial (comparação justa) ---- let (logits_fin, _, _) = passo_um(&ger, tok, &cod_cal, &mut rng, &dev)?; let plano_fin = logits_fin.reshape((cfg.treino.lote * n_l_alvo, ger.k))?; let conf_fin = conf_media(&plano_fin, &verificador)?; let ce_final = ce_por_nivel_medio(&ger, tok, &cod_cal, &mut rng)?; let alvo_fin = cod_cal.ids[nivel_alvo] .reshape((cfg.treino.lote * n_l_alvo,))? .to_dtype(candle_core::DType::U32)?; let (_, ece_fin) = verificador.ajustar(&plano_fin, &alvo_fin)?; let ece_ini = hub.series.get("ece_calibracao") .and_then(|s| s.first().map(|(_, v)| *v)).unwrap_or(0.0); let _ = tau_c0; hub.dump_jsonl(); Ok(ResumoFaseC { passos: passos_feitos, ce_inicial, ce_final, ce_gatilhada_sem_correcao: media(&ce_gat_sem_hist), ce_gatilhada_com_correcao: media(&ce_gat_com_hist), confianca_media_inicial: conf_inicial, confianca_media_final: conf_fin, confianca_pos_correcao: media(&conf_pos_hist), taxa_gatilho: media(&taxa_hist), ece_antes: ece_ini, ece_depois: ece_fin, temperatura_calibrada: verificador.temperatura, tau_c: auto.tau_c, taxa_conflito_pcgrad: conflitos as f64 / pares_grad.max(1) as f64, decaimento_delta: decaimento_geometrico(&normas_delta), acordos_medios: media(&acordos), escore_aprendizado: hub.escore_aprendizado(), rodadas_acoplador: acoplador.telemetria.rodadas, timeouts_acoplador: acoplador.telemetria.timeouts, maior_sobreposicao_us: acoplador.telemetria.maior_sobreposicao_us, }) } // ---- funções auxiliares da fase C ----------------------------------------- fn media(v: &[f64]) -> f64 { v.iter().sum::() / v.len().max(1) as f64 } /// Corrompe códigos com a taxa κ do gerador (mesma distribuição da fase B). fn ger_corromper( ger: &GeradorConceitos, codigos: &[Tensor], b: usize, rng: &mut StdRng, dev: &Device, ) -> Result> { let mut saida = Vec::with_capacity(codigos.len()); for (i, cid) in codigos.iter().enumerate() { let mut c = cid.clone(); if ger.corrupcao > 0.0 { let plano = c.reshape((b * ger.niveis[i],))?; let mut v = plano.flatten_all()?.to_vec1::()?; for x in v.iter_mut() { if rng.gen::() < ger.corrupcao { *x = rng.gen_range(0..ger.k as u32); } } c = Tensor::from_vec(v, (b, ger.niveis[i]), dev)?; } saida.push(c); } Ok(saida) } /// Monta a sequência [prefixo; níveis] com condicionamentos dados; `z_alvo` /// substitui o condicionamento do nível `nivel_substituido`. fn montar_sequencia( ger: &GeradorConceitos, zhats: &[Tensor], z_substituto: Option<(&Tensor, usize)>, b: usize, dev: &Device, ) -> Result { let mut blocos = Vec::with_capacity(ger.niveis.len()); for nivel in 0..ger.niveis.len() { if let Some((z, n_sub)) = z_substituto { if nivel == n_sub { blocos.push(ger.entrada_nivel_pub(Some(z), nivel, b, dev)?); continue; } } let z_ant = if nivel == 0 { None } else { Some(&zhats[nivel - 1]) }; blocos.push(ger.entrada_nivel_pub(z_ant, nivel, b, dev)?); } let prefixo_ids = Tensor::zeros((b, crate::gerador::N_PREFIXO), candle_core::DType::U32, dev)?; let prefixo = ger.emb_niveis.tabela.fwd(&prefixo_ids)?; let mut seqs = vec![prefixo]; seqs.extend(blocos.iter().cloned()); Tensor::cat(&seqs, 1) } fn niveis_pos_de(ger: &GeradorConceitos) -> Vec { let mut niveis_pos = vec![0usize; crate::gerador::N_PREFIXO]; for (i, &n_l) in ger.niveis.iter().enumerate() { niveis_pos.extend(std::iter::repeat(i + 1).take(n_l)); } niveis_pos } /// Forward do tronco sobre a sequência montada. fn forward_tronco(ger: &GeradorConceitos, s: &Tensor) -> Result { let mut resumos = Vec::new(); let niveis_pos = niveis_pos_de(ger); ger.tronco.forward_treino(s, &ger.mascara, &niveis_pos, &mut resumos) } /// PASSO-1: UMA corrupção κ → forward → (logits do nível-alvo (b, n_L, K), /// resumo do tronco do nível anterior (b, d_modelo) DESTACADO, zhats /// corrompidos REUTILIZÁVEIS — função apenas de c_{ Result<(Tensor, Tensor, Vec)> { let b = cod.ids[0].dim(0)?; let cod_uso = ger_corromper(ger, &cod.ids, b, rng, dev)?; let zhats = tok.reconstruir_de_codigos(&cod_uso, cod_uso.len(), b, dev)?; let s = montar_sequencia(ger, &zhats, None, b, dev)?; let h = forward_tronco(ger, &s)?; let nivel_alvo = ger.niveis.len() - 1; let inicio = crate::gerador::N_PREFIXO + ger.niveis.iter().take(nivel_alvo).sum::(); let n_l = ger.niveis[nivel_alvo]; let trecho = h.narrow(1, inicio, n_l)?; let logits = ger.cabeca.cabecas[nivel_alvo].fwd(&trecho)?; // resumo do nível ANTERIOR (conhecimento a — do passado), destacado: // média do estado oculto sobre as posições do nível ℓ−1 (função de c_{(); let trecho_ant = h.narrow(1, ini_ant, ger.niveis[nivel_alvo - 1])?; trecho_ant.mean(1)?.detach() // (b, d_modelo) }; Ok((logits, resumo_ant, zhats)) } /// PASSO-2 (logits): forward com condicionamento CORRIGIDO no nível-alvo; /// reusa os mesmos zhats corrompidos do passo-1 (comparação controlada). fn passo_dois_logits( ger: &GeradorConceitos, tok: &TokenizadorConceitos, cod: &crate::tokenizador::Codigos, zhats: &[Tensor], z_corrigido: &Tensor, nivel_alvo: usize, ) -> Result { let _ = tok; let b = cod.ids[0].dim(0)?; let dev = z_corrigido.device(); let s = montar_sequencia(ger, zhats, Some((z_corrigido, nivel_alvo)), b, dev)?; let h = forward_tronco(ger, &s)?; let inicio = crate::gerador::N_PREFIXO + ger.niveis.iter().take(nivel_alvo).sum::(); let n_l = ger.niveis[nivel_alvo]; let trecho = h.narrow(1, inicio, n_l)?; ger.cabeca.cabecas[nivel_alvo].fwd(&trecho) } /// PASSO-2 (perda): CE média por nível com o condicionamento corrigido. fn passo_dois_perda( ger: &GeradorConceitos, tok: &TokenizadorConceitos, cod: &crate::tokenizador::Codigos, zhats: &[Tensor], z_corrigido: &Tensor, rng: &mut StdRng, ) -> Result { let _ = tok; let b = cod.ids[0].dim(0)?; let dev = z_corrigido.device(); let nivel_alvo = ger.niveis.len() - 1; let s = montar_sequencia(ger, zhats, Some((z_corrigido, nivel_alvo)), b, dev)?; let h = forward_tronco(ger, &s)?; let _ = rng; let mut ces = Vec::with_capacity(ger.niveis.len()); let mut inicio = crate::gerador::N_PREFIXO; for (i, &n_l) in ger.niveis.iter().enumerate() { let trecho = h.narrow(1, inicio, n_l)?; let logits = ger.cabeca.cabecas[i].fwd(&trecho)?; let plano = logits.reshape((b * n_l, ger.k))?; let alvo = cod.ids[i].reshape(b * n_l)?.to_dtype(candle_core::DType::U32)?; ces.push(cross_entropy(&plano, &alvo)?); inicio += n_l; } let l = ces.len() as f64; let mut perda = ces[0].affine(1.0 / l, 0.0)?; for ce in &ces[1..] { perda = (&perda + &ce.affine(1.0 / l, 0.0)?)?; } Ok(perda) } /// CE média nas LINHAS gatilhadas (logits (b, n_L, K), alvo (b, n_L)). fn ce_nas_linhas(logits: &Tensor, alvo: &Tensor, gatilho: &[bool]) -> Result { let b = logits.dim(0)?; let n_l = logits.dim(1)?; let k = logits.dim(2)?; let mut soma = 0f64; let mut n = 0usize; for (bi, g) in gatilho.iter().enumerate() { if !*g { continue; } n += 1; let linha = logits.narrow(0, bi, 1)?.reshape((n_l, k))?; let alvo_l = alvo.narrow(0, bi, 1)?.reshape((n_l,))? .to_dtype(candle_core::DType::U32)?; soma += cross_entropy(&linha, &alvo_l)?.to_scalar::()? as f64; } Ok(soma / n.max(1) as f64) } /// Confiança calibrada média de um plano (n, K). fn conf_media(plano: &Tensor, v: &crate::verificador::VerificadorNcm) -> Result { let confs = v.confianca(plano)?; Ok(confs.mean_all()?.to_scalar::()? as f64) } /// Taxa de gatilho (fração de linhas com conf < τ_c). fn taxa_gatilho( plano: &Tensor, v: &crate::verificador::VerificadorNcm, tau: f64, ) -> Result { let confs = v.confianca(plano)?; let c_v: Vec = confs.flatten_all()?.to_vec1::()? .iter().map(|x| *x as f64).collect(); Ok(c_v.iter().filter(|c| **c < tau).count() as f64 / c_v.len().max(1) as f64) } /// CE média por nível (avaliação). fn ce_por_nivel_medio( ger: &GeradorConceitos, tok: &TokenizadorConceitos, cod: &crate::tokenizador::Codigos, rng: &mut StdRng, ) -> Result { let s = ger.perda_treino(tok, &cod.ids, rng)?; Ok(s.ce_por_nivel.iter().sum::() / s.ce_por_nivel.len() as f64) } /// Projeta (b, d_modelo) → (b, d_lat) por média de colunas agrupadas /// (projeção fixa, sem parâmetros — determinística e causal). fn projeta_para_d_lat(x: &Tensor, d_lat: usize, dev: &Device) -> Result { let b = x.dim(0)?; let d = x.dim(1)?; if d == d_lat { return Ok(x.clone()); } if d % d_lat != 0 { let mut v = x.flatten_all()?.to_vec1::()?; let mut linhas = Vec::new(); for i in 0..b { let mut linha = v[i * d..(i + 1) * d].to_vec(); while linha.len() % d_lat != 0 { linha.push(0.0); } linhas.push(linha); } let d2 = linhas[0].len(); let t = Tensor::from_vec(linhas.into_iter().flatten().collect(), (b, d2), dev)?; return projeta_para_d_lat(&t, d_lat, dev); } let g = d / d_lat; let v = x.flatten_all()?.to_vec1::()?; let mut out = Vec::with_capacity(b * d_lat); for i in 0..b { for j in 0..d_lat { let mut s = 0f32; for k in 0..g { s += v[i * d + j * g + k]; } out.push(s / g as f32); } } Ok(Tensor::from_vec(out, (b, d_lat), dev)?) } /// Acordo par-a-par das tentativas: média sobre o lote da similaridade /// de cosseno média entre pares (b, n, d) → [0, 1]. fn acordo_tentativas(candidatos: &Tensor) -> Result { let b = candidatos.dim(0)?; let n = candidatos.dim(1)?; let v = candidatos.to_vec3::()?; let mut soma = 0f64; let mut pares = 0usize; for bi in 0..b { for i in 0..n { for j in i + 1..n { let (mut dot, mut ni, mut nj) = (0f32, 0f32, 0f32); for k in 0..v[bi][i].len() { dot += v[bi][i][k] * v[bi][j][k]; ni += v[bi][i][k] * v[bi][i][k]; nj += v[bi][j][k] * v[bi][j][k]; } let cos = (dot / (ni.sqrt() * nj.sqrt() + 1e-8)) as f64; soma += (cos + 1.0) / 2.0; // [−1,1] → [0,1] pares += 1; } } } Ok(soma / pares.max(1) as f64) } /// Produto interno global entre dois gradientes achatados (taxa de conflito /// PCGrad). Devolve (⟨g1,g2⟩, true) se ambos densos. fn produto_interno_grads( g1: &[Option>], g2: &[Option>], ) -> (f64, bool) { let mut ip = 0f64; let mut denso = true; for (a, b) in g1.iter().zip(g2.iter()) { match (a, b) { (Some(x), Some(y)) => { for k in 0..x.len() { ip += x[k] as f64 * y[k] as f64; } } _ => denso = false, } } (ip, denso) } /// Razão geométrica de decaimento das normas de δ (primeira→última metade). fn decaimento_geometrico(v: &[f64]) -> f64 { if v.len() < 4 { return 1.0; } let m = v.len() / 2; let (a, bb) = (media(&v[..m]), media(&v[m..])); if a > 1e-12 { (bb / a).clamp(0.0, 10.0) } else { 1.0 } } #[cfg(test)] mod testes_v090 { use super::*; use candle_nn::optim::{AdamW as AdamWCandle, Optimizer, ParamsAdamW}; /// **X-3 (oráculo independente — crítica Qwen3.8 nº 2, r-34 §2):** treina /// o MESMO problema convexo (regressão linear por MSE) com o nosso /// `AdamWManual` e com o `candle_nn::optim::AdamW` do próprio framework. /// As fórmulas são idênticas (AdamW desacoplado), mas a ORDEM de ops difere /// (divisão por bc vs multiplicação pelo recíproco; ponto flutuante não é /// associativo) ⇒ a igualdade é em ε, não bit-a-bit. Se as trajetórias /// divergissem além de ε, o nosso otimizador teria um bug de fórmula. #[test] fn adamw_oraculo_candle_nn() { let dev = Device::Cpu; // dados determinísticos: x (128, 8), alvo = x @ W* (sem ruído) let n = 128usize; let d = 8usize; let k = 4usize; let xs: Vec = (0..n * d).map(|i| ((i as f32) * 0.37) % 1.0 - 0.5).collect(); let x = Tensor::from_vec(xs, (n, d), &dev).unwrap(); let w_star: Vec = (0..d * k).map(|i| ((i as f32) * 0.11).sin() * 0.5).collect(); let w_star_t = Tensor::from_vec(w_star.clone(), (d, k), &dev).unwrap(); let y = x.matmul(&w_star_t).unwrap(); // mesma inicialização para os dois otimizadores (determinística) let w_inicial: Vec = (0..d * k).map(|i| ((i as f32) * 0.7) % 0.2 - 0.1).collect(); let (lr, wd) = (0.05f64, 0.0); let roda = |usar_nosso: bool| -> Vec { let w_t = Tensor::from_vec(w_inicial.clone(), (d, k), &dev).unwrap(); let w_var = Var::from_tensor(&w_t).unwrap(); let vars = vec![w_var.clone()]; let mut nosso = AdamWManual::novo(vars.clone(), lr, wd); let params = ParamsAdamW { lr, beta1: 0.9, beta2: 0.999, eps: 1e-8, weight_decay: wd }; let mut candle_opt = AdamWCandle::new(vars.clone(), params).unwrap(); for passo in 0..60 { let predita = x.matmul(w_var.as_tensor()).unwrap(); let erro = predita.sub(&y).unwrap(); let perda = erro.sqr().unwrap().mean_all().unwrap(); if usar_nosso { let grads = grads_achatados(&perda, &vars).unwrap(); // sem clip: comparação de FÓRMULA, não do mecanismo de segurança nosso.passo(grads); } else { let gs = perda.backward().unwrap(); candle_opt.step(&gs).unwrap(); } let _ = passo; } w_var .as_tensor() .flatten_all() .unwrap() .to_vec1::() .unwrap() }; let fim_nosso = roda(true); let fim_candle = roda(false); let mut max_dif = 0f32; for (a, b) in fim_nosso.iter().zip(fim_candle.iter()) { max_dif = max_dif.max((a - b).abs()); } let escala = fim_nosso.iter().map(|v| v.abs()).fold(0f32, f32::max).max(1e-6); assert!( max_dif / escala < 1e-4, "AdamWManual divergiu do oráculo candle_nn: max|Δ|={max_dif} (escala {escala})" ); // e ambos convergiram perto do mínimo (o alvo é exato) let erro_final = fim_nosso .iter() .zip(w_star.iter()) .map(|(a, b)| (a - b).abs()) .fold(0f32, f32::max); assert!(erro_final < 0.05, "regressão não convergiu: {erro_final}"); } }