khtst-rust / src /treino.rs
PowerMachine's picture
v0.9.0: análise/prova/teste das críticas Qwen3.8 lote 2 (r-34) — SIMD com despacho em runtime bit-a-bit (X-1), oráculo candle_nn::optim::AdamW (nosso 1,53× mais rápido), Thompson Sampling (regret 0,0019 vs UCB1 0,0156), GNG por entropia com piso (T-G1'), JEPA de fração adaptativa (bit-a-bit com 1 braço), configs tipadas, entrada numpy (2008× na fronteira), avaliação com dados reais — 104/104 gates, 111/111 cargo
d444358 verified
Raw History Blame Contribute Delete
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
}
}
#[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<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}");
}
}