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/treino.rs from PowerMachine/khtst-rust: direct link, hf CLI and curl.
- Browser
- Download file 50.4 kB
-
https://huggingface.co/PowerMachine/khtst-rust/resolve/main/src/treino.rs
- Command line
-
hf download hf://PowerMachine/khtst-rust/src/treino.rs
-
curl -L -o treino.rs https://huggingface.co/PowerMachine/khtst-rust/resolve/main/src/treino.rs
50.4 kB
| //! 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<usize, Vec<f32>>, | |
| pub v: HashMap<usize, Vec<f32>>, | |
| pub t: usize, | |
| pub lr: f64, | |
| pub beta1: f64, | |
| pub beta2: f64, | |
| pub eps: f64, | |
| pub wd: f64, | |
| pub vars: Vec<Var>, | |
| } | |
| impl AdamWManual { | |
| pub fn novo(vars: Vec<Var>, 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<Option<Vec<f32>>>) { | |
| 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::<f32>().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<Vec<f32>>]) -> 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<Vec<f32>>], 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<Vec<Option<Vec<f32>>>> { | |
| 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::<f32>()?)), | |
| 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<Option<Vec<f32>>>>) -> Vec<Option<Vec<f32>>> { | |
| 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<Option<Vec<f32>>> = 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<f32> = 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<Vec<u32>>, // cada uma de comprimento N (= ctx) | |
| } | |
| impl DadosTreino { | |
| pub fn novo(sequencias: Vec<Vec<u32>>, 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<Tensor> { | |
| 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<u32> = 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<pyo3::Py<pyo3::PyAny>>; | |
| 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<f64>, | |
| ) -> Result<ResumoFase> { | |
| 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::<f32>()? 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::<f32>()? 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::<f64>() / 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::<f64>() / 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<f64>, | |
| ) -> Result<ResumoFase> { | |
| 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::<f64>() / 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::<f64>() / 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::<f64>() / 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<f64>, | |
| passos: usize, | |
| caminho_gerador: Option<&str>, | |
| ) -> Result<ResumoFaseC> { | |
| 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<f32> = (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<f64> = crate::verificador::confianca_por_linha(&plano)? | |
| .flatten_all()?.to_vec1::<f32>()?.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<f64> = Vec::new(); | |
| let mut acordos: Vec<f64> = Vec::new(); | |
| let mut ce_gat_sem_hist: Vec<f64> = Vec::new(); | |
| let mut ce_gat_com_hist: Vec<f64> = Vec::new(); | |
| let mut conf_pos_hist: Vec<f64> = Vec::new(); | |
| let mut taxa_hist: Vec<f64> = 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<f64> = crate::verificador::confianca_por_linha(&plano1)? | |
| .flatten_all()?.to_vec1::<f32>()?.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<bool> = 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<f32> = 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::<f32>()? 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::<f32>()?, | |
| b: z_ant.get(0)?.get(0)?.to_vec1::<f32>()?, | |
| 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<f32> = delta.get(0)?.to_vec1::<f32>()?; | |
| let norma_delta = | |
| (delta.sqr()?.sum_all()?.to_scalar::<f32>()? 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::<f64>() / 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::<f32>()? 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::<f64>() / 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::<f64>() / 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::<f64>() / 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<Vec<Tensor>> { | |
| 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::<u32>()?; | |
| for x in v.iter_mut() { | |
| if rng.gen::<f64>() < 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<Tensor> { | |
| 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<usize> { | |
| 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<Tensor> { | |
| 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_{<L} (causal)). | |
| fn passo_um( | |
| ger: &GeradorConceitos, | |
| tok: &TokenizadorConceitos, | |
| cod: &crate::tokenizador::Codigos, | |
| rng: &mut StdRng, | |
| dev: &Device, | |
| ) -> Result<(Tensor, Tensor, Vec<Tensor>)> { | |
| 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::<usize>(); | |
| 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_{<L}) | |
| let resumo_ant = { | |
| let ini_ant = crate::gerador::N_PREFIXO | |
| + ger.niveis.iter().take(nivel_alvo - 1).sum::<usize>(); | |
| 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<Tensor> { | |
| 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::<usize>(); | |
| 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<Tensor> { | |
| 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<f64> { | |
| 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::<f32>()? 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<f64> { | |
| let confs = v.confianca(plano)?; | |
| Ok(confs.mean_all()?.to_scalar::<f32>()? as f64) | |
| } | |
| /// Taxa de gatilho (fração de linhas com conf < τ_c). | |
| fn taxa_gatilho( | |
| plano: &Tensor, | |
| v: &crate::verificador::VerificadorNcm, | |
| tau: f64, | |
| ) -> Result<f64> { | |
| let confs = v.confianca(plano)?; | |
| let c_v: Vec<f64> = confs.flatten_all()?.to_vec1::<f32>()? | |
| .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<f64> { | |
| let s = ger.perda_treino(tok, &cod.ids, rng)?; | |
| Ok(s.ce_por_nivel.iter().sum::<f64>() / 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<Tensor> { | |
| 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::<f32>()?; | |
| 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::<f32>()?; | |
| 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<f64> { | |
| let b = candidatos.dim(0)?; | |
| let n = candidatos.dim(1)?; | |
| let v = candidatos.to_vec3::<f32>()?; | |
| 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<Vec<f32>>], | |
| g2: &[Option<Vec<f32>>], | |
| ) -> (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 | |
| } | |
| } | |
| 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. | |
| 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<f32> = (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<f32> = (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<f32> = (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<f32> { | |
| 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::<f32>() | |
| .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}"); | |
| } | |
| } | |