File size: 53,408 Bytes
d444358 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 | //! 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,
}
#[derive(Debug, Clone)]
pub enum EventoCiclo {
Planejamento { iteracao: usize, temperatura: f64 },
DistribuicaoCompetencias { por_tentativa: Vec<f64> },
RePlanejamento { motivo: String },
Resposta { confianca: f64, iteracoes: usize },
}
#[derive(Debug, Clone)]
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).
#[derive(Clone, Copy, Debug)]
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.
#[derive(Debug, Clone)]
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.
#[derive(Debug, Clone)]
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)
}
#[cfg(test)]
mod testes {
use super::*;
#[test]
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}");
}
#[test]
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}");
}
}
#[test]
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");
}
#[test]
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}");
}
#[test]
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));
}
#[test]
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).
#[test]
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.
#[test]
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).
#[test]
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).
#[test]
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).
#[test]
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(())
}
}
|