v0.10.0: análise/prova/teste das críticas de engenharia Qwen3.8 lote 3 (r-35) — divisão SRP dos monólitos jepa/pensamento/moe/python em 26 submódulos (API estável via re-exports; 'Amdahl de compilação' refutada por experimento controlado, builds frios neutros), checkpoints atômicos SHA-256 puro FIPS 180-4 + fsync + rename + manifesto (CP-1..CP-3; correções da proposta: sem destruir estado anterior, manifesto com barreira, sem bincode/unwrap), TemperaturaInversa T-V3 (fronteira tipada da convexidade N-2; alegação compile-time corrigida), trait CamadaKhtst+CamadaHomeostatica (premissa do controlador refutada no fonte; Vec<Var>; adaptador MoeComPapel bit-a-bit), CLI Typer 75 (fachada aditiva com resume verificado por hash) — 123/123 cargo, 107/107 gates, treino real Wikipédia PT-BR com checkpoints íntegros
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Cargo.lock +1 -1
- Cargo.toml +2 -2
- docs/memorias/r-35-analise-provas-qwen-v010.md +360 -0
- pyproject.toml +2 -2
- resultados/fase_a_v010.json +14 -0
- resultados/fase_b_v010.json +11 -0
- resultados/suite_v010_final.json +159 -0
- resultados/telemetria_fase_c.jsonl +0 -0
- resultados/telemetria_v010.json +155 -0
- resultados/treino_v090.json +26 -26
- scripts/75_cli.py +191 -0
- scripts/76_telemetria_v010.py +134 -0
- scripts/77_suite_v010.py +141 -0
- scripts/78_publicar_v010.py +194 -0
- src/camadas.rs +162 -0
- src/checkpoint.rs +353 -0
- src/fnh.rs +7 -0
- src/gerador.rs +14 -5
- src/jepa/mascaras.rs +173 -0
- src/jepa/mod.rs +61 -0
- src/jepa/nucleo.rs +211 -0
- src/jepa/patches.rs +150 -0
- src/jepa/replay_ssm.rs +107 -0
- src/jepa/sonic_moe.rs +155 -0
- src/jepa/testes.rs +212 -0
- src/{jepa.rs → jepa/variantes.rs} +3 -1016
- src/lib.rs +1 -0
- src/moe_redirecionavel/atencao.rs +63 -0
- src/moe_redirecionavel/mod.rs +57 -0
- src/{moe_redirecionavel.rs → moe_redirecionavel/nucleo.rs} +3 -779
- src/moe_redirecionavel/teste_integrado.rs +26 -0
- src/moe_redirecionavel/testes.rs +209 -0
- src/moe_redirecionavel/treino.rs +463 -0
- src/pensamento/combinador.rs +73 -0
- src/pensamento/mod.rs +71 -0
- src/{pensamento.rs → pensamento/motor.rs} +4 -647
- src/pensamento/quebra_cabeca.rs +233 -0
- src/pensamento/testes.rs +303 -0
- src/python.rs +0 -0
- src/python/calibracao.rs +134 -0
- src/python/demos_qwen.rs +359 -0
- src/python/fnh_arvore.rs +344 -0
- src/python/jepa_treinos.rs +251 -0
- src/python/mod.rs +145 -0
- src/python/moe.rs +190 -0
- src/python/nlp_nlg.rs +69 -0
- src/python/nucleo.rs +239 -0
- src/python/pensamento_demos.rs +367 -0
- src/python/som.rs +180 -0
- src/python/treino_fases.rs +228 -0
Cargo.lock
CHANGED
|
@@ -723,7 +723,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|
| 723 |
|
| 724 |
[[package]]
|
| 725 |
name = "khtst-rust"
|
| 726 |
-
version = "0.
|
| 727 |
dependencies = [
|
| 728 |
"anyhow",
|
| 729 |
"built",
|
|
|
|
| 723 |
|
| 724 |
[[package]]
|
| 725 |
name = "khtst-rust"
|
| 726 |
+
version = "0.10.0"
|
| 727 |
dependencies = [
|
| 728 |
"anyhow",
|
| 729 |
"built",
|
Cargo.toml
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
[package]
|
| 2 |
name = "khtst-rust"
|
| 3 |
-
version = "0.
|
| 4 |
edition = "2021"
|
| 5 |
-
description = "KHTST_RUST —
|
| 6 |
license = "Apache-2.0"
|
| 7 |
|
| 8 |
[lib]
|
|
|
|
| 1 |
[package]
|
| 2 |
name = "khtst-rust"
|
| 3 |
+
version = "0.10.0"
|
| 4 |
edition = "2021"
|
| 5 |
+
description = "KHTST_RUST — críticas Qwen3.8 lote 3 (r-35): divisão SRP dos monólitos, checkpoints atômicos SHA-256+fsync+rename, newtype TemperaturaInversa (fronteira tipada N-2), trait CamadaKhtst + CamadaHomeostatica, CLI unificada Typer"
|
| 6 |
license = "Apache-2.0"
|
| 7 |
|
| 8 |
[lib]
|
docs/memorias/r-35-analise-provas-qwen-v010.md
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# r-35 — Análise, Provas e Testes das Críticas de Engenharia do Agente Qwen3.8 (v0.10.0, lote 3)
|
| 2 |
+
|
| 3 |
+
**Escopo**: o usuário pediu *analisar, provar e testar* o terceiro lote de
|
| 4 |
+
críticas do agente Qwen3.8 (anti-padrões de engenharia de software que
|
| 5 |
+
"comprometem a escalabilidade": monólitos Rust, scripts Python
|
| 6 |
+
sequenciais, checkpointing não-atômico, invariantes apenas em
|
| 7 |
+
comentários), mantendo as regras da casa: **nenhuma funcionalidade SOTA
|
| 8 |
+
eficaz é removida**; toda mudança com prova matemática, lógica e de
|
| 9 |
+
lógica computacional; provas em markdown; validação com dataset real;
|
| 10 |
+
autocrítica (inclusive das minhas próprias correções).
|
| 11 |
+
|
| 12 |
+
**Metodologia** (herdada da r-34): (i) verificação simbólica/lógica de
|
| 13 |
+
cada crítica e proposta ANTES do código; (ii) confronto com oráculos
|
| 14 |
+
independentes (medições frias em target dir separado, experimento
|
| 15 |
+
controlado com grupo de controle, vetores oficiais FIPS 180-4); (iii)
|
| 16 |
+
implementação apenas do que sobrevive a (i)+(ii), como **adição**; (iv)
|
| 17 |
+
testes de regressão + suíte consolidada; (v) validação fim-a-fim com
|
| 18 |
+
dataset real (Wikipédia PT-BR, streaming controlado).
|
| 19 |
+
|
| 20 |
+
**Veredito sumário**:
|
| 21 |
+
|
| 22 |
+
| # | Crítica / Proposta | Veredito | Ação v0.10.0 | Ganho medido |
|
| 23 |
+
|---|--------------------|----------|--------------|--------------|
|
| 24 |
+
| 1 | Monólitos jepa.rs (58,4 kB), pensamento.rs (53,4 kB), moe_redirecionavel.rs (50,6 kB) violam SRP; "Lei de Amdahl p/ compilação": split ⇒ T_inc ≈ O(N/k) | **SRP VÁLIDO; a "prova" é uma APLICAÇÃO ERRADA de Amdahl e a alegação O(N/k) é REFUTADA por experimento controlado** | divisão em 26 submódulos coesos com re-exports (API estável); +python.rs 112 kB (pior monólito — acrescentado por autocrítica) | 61 arquivos, maior agora 57,6 kB (fnh.rs); 107/107 gates herdados preservados; compilação incremental ~1,1 s ANTES E DEPOIS (ruído); builds frios neutros (70→68 s, 85→86 s) |
|
| 25 |
+
| 2 | "Scripts Python sequenciais" (39_coletar…74) ⇒ substituir por CLI Typer | **PARCIALMENTE VÁLIDA como desejo de unificação; a SUBSTITUIÇÃO é REJEITADA** (quebra rastreabilidade r-XX↔scripts e remove funcionalidade) | `scripts/75_cli.py` — fachada Typer que delega aos scripts canônicos + política de resume com integridade + gestão de estados | 1 entry point p/ todo o pipeline; resume com prova SHA-256; drill de corrupção 3/3 |
|
| 26 |
+
| 3 | Checkpointing não-atômico (`os.path.exists` vulnerável a queda de energia) | **VÁLIDA no problema; a proposta tem 3 falhas** (destrói o estado anterior; manifesto sem fsync; bincode+`unwrap`) | `src/checkpoint.rs`: SHA-256 puro (FIPS 180-4) + tmp→fsync→rename→fsync(dir) p/ conteúdo E manifesto; `carregar_verificado`; bindings Python | 2 checkpoints do treino real verificados (240,9 MB e 59,5 MB, manifesto 141 B); adulteração/truncamento/manifesto-trocado detectados; estado anterior preservado sob falha simulada |
|
| 27 |
+
| 4 | "Invariantes apenas em comentários" (convexidade em γ=1/T) ⇒ newtype `TemperaturaInversa` | **VÁLIDA no padrão; a alegação "garantia em tempo de COMPILAÇÃO" é FALSA** (a validação é de runtime na fronteira) | `tipos::TemperaturaInversa` (T-V3) + fronteira tipada no verificador (`nll_temperatura_tipado`, setter/getter, saída do otimizador validada) | T* do ajuste validada por construção; equivalência bit-a-bit com o caminho cru (tv3d); pré-condição do Teo N-2 carregada no tipo |
|
| 28 |
+
| P1 | Trait `CamadaKhtst` c/ `forward`+`parametros`+`gate_ativacao` | **IDEIA VÁLIDA; PREMISSA FALSA (controlador NÃO conhece Fnh/Moe/Jepa — verificado no fonte) e assinatura quebraria o otimizador** | trait corrigido (`Vec<Var>`; gate segregado em `CamadaHomeostatica`; adaptador `MoeComPapel` captura o papel) | composição heterogênea preserva B×T (CK-1); delegação bit-a-bit (CK-1c); despacho homeostático tipado (CK-2a) |
|
| 29 |
+
| P3 | Checkpoint atômico com "prova criptográfica" | ver #3 acima | idem | idem |
|
| 30 |
+
| P4 | CLI Typer declarativo | ver #2 acima | idem | idem |
|
| 31 |
+
|
| 32 |
+
**Contagem**: 4 críticas + 4 propostas analisadas; **2 aceitas com
|
| 33 |
+
correções substanciais**, **1 aceita parcialmente (substância sim,
|
| 34 |
+
substituição não)**, **1 com premissa refutada por experimento**, e **todas
|
| 35 |
+
as 4 propostas de código continham erros** catalogados abaixo. 16 testes
|
| 36 |
+
novos; suíte consolidada **107/107**; cargo **123/123**.
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
## §1 — Ambiente, baseline e higiene (pré-requisitos do usuário)
|
| 41 |
+
|
| 42 |
+
- **Processos de fundo**: nenhum processo residual de treino encontrado
|
| 43 |
+
(`ps aux` na abertura); toda execução em primeiro plano, como exige a
|
| 44 |
+
advertência de que o fundo mata processos sem erro/OOM (confirmado
|
| 45 |
+
empiricamente nas sessões anteriores — r-32 §"fase C com resume").
|
| 46 |
+
- **Estados de treino**: varredura inicial (`*.pt`, `*.npz`, `*.ckpt`,
|
| 47 |
+
`*.safetensors`, `*.estado.json`) = **0 estados locais**; um
|
| 48 |
+
`telemetria_fase_c.jsonl` da v0.9.0 (estado residual de treino
|
| 49 |
+
anterior, exatamente o que o usuário manda procurar) foi **encontrado
|
| 50 |
+
e apagado** antes do treino fresco v0.10.0 (§5).
|
| 51 |
+
- **Espaço**: 3,1 GB de alvos cargo obsoletos (`logs/tgt_full`,
|
| 52 |
+
`logs/tgt_domain` — artefatos de builds de sessões passadas) apagados
|
| 53 |
+
(96% → 64% de uso do disco).
|
| 54 |
+
- **Baseline**: v0.9.0 reconstruída e verificada **107/107** antes de
|
| 55 |
+
qualquer mudança.
|
| 56 |
+
|
| 57 |
+
## §2 — Crítica 1: "Monólitos em Rust" e a "Lei de Amdahl para Compilação"
|
| 58 |
+
|
| 59 |
+
### 2.1 Análise da crítica
|
| 60 |
+
|
| 61 |
+
**Válido (substância)**: `jepa.rs` (58.407 B), `pensamento.rs` (53.408 B)
|
| 62 |
+
e `moe_redirecionavel.rs` (50.941 B) eram arquivos multi-responsabilidade
|
| 63 |
+
— `jepa.rs` continha núcleo E/P/EMA, 6 famílias de máscara, 6
|
| 64 |
+
embaralhamentos de patches, enum de 10 variantes + treinos especializados,
|
| 65 |
+
SonicMoE E ReplaySSM. Violação real de SRP.
|
| 66 |
+
|
| 67 |
+
**Inválido (rótulo)**: a "Prova Lógica (Lei de Amdahl para Compilação)"
|
| 68 |
+
não é a Lei de Amdahl. A Lei de Amdahl limita o ganho de paralelização
|
| 69 |
+
pela fração serial: S(n) = 1/((1−p) + p/n). O que a crítica quer invocar
|
| 70 |
+
é a granularidade da **unidade de compilação** — e a afirmação
|
| 71 |
+
"T_inc do monólito é O(N); dividir em módulos reduz T_inc ≈ O(N/k)" é
|
| 72 |
+
**falsa na granularidade proposta**:
|
| 73 |
+
|
| 74 |
+
**Teo CK-0 (granularidade de recompilação).** O cargo/rustc recompila por
|
| 75 |
+
**crate**, não por arquivo. Dividir `jepa.rs` em `jepa/*.rs` não cria
|
| 76 |
+
unidades de compilação novas: um toque em qualquer arquivo do diretório
|
| 77 |
+
recompila o crate inteiro (com reuso incremental por queries/itens). Para
|
| 78 |
+
obter T_inc ≈ O(N/k) seria necessário dividir em **k crates** (workspace),
|
| 79 |
+
o que tem custos próprios: unificação de features entre crates, a `cdylib`
|
| 80 |
+
única do PyO3 exigiria um crate só de FFI dependendo de crates de domínio,
|
| 81 |
+
e qualquer mudança de API pública recompila os dependentes (o grafo de
|
| 82 |
+
dependências domina — de novo Amdahl, agora corretamente aplicado: o ganho
|
| 83 |
+
é limitado pelo caminho crítico do DAG de crates). ∎
|
| 84 |
+
|
| 85 |
+
### 2.2 O que foi feito (divisão SRP aditiva)
|
| 86 |
+
|
| 87 |
+
- `jepa.rs` → `jepa/{nucleo, mascaras, patches, variantes, sonic_moe,
|
| 88 |
+
replay_ssm, testes}.rs`
|
| 89 |
+
- `pensamento.rs` → `pensamento/{motor, combinador, quebra_cabeca, testes}.rs`
|
| 90 |
+
- `moe_redirecionavel.rs` → `moe_redirecionavel/{atencao, nucleo, treino,
|
| 91 |
+
testes, teste_integrado}.rs`
|
| 92 |
+
- `python.rs` (112.410 B — o **pior** monólito, não citado pela crítica:
|
| 93 |
+
errata por autocrítica) → `python/{nucleo, treino_fases, som,
|
| 94 |
+
jepa_treinos, pensamento_demos, calibracao, moe, fnh_arvore, nlp_nlg,
|
| 95 |
+
demos_qwen}.rs`
|
| 96 |
+
- Cada `mod.rs` re-exporta `pub use sub::*;` ⇒ **todos os caminhos
|
| 97 |
+
`crate::jepa::X` etc. permanecem válidos** (107/107 testes herdados
|
| 98 |
+
verdes = prova de preservação de comportamento).
|
| 99 |
+
- Submódulos do python/ ganham `pub(crate)` nas pyfunctions/pyclasses (o
|
| 100 |
+
registro `#[pymodule]` referencia `sub::item`); helpers entre irmãos
|
| 101 |
+
(`distancia_eucl`, `carregar_tokenizador`, `ConfigPy`) promovidos a
|
| 102 |
+
`pub(crate)` — visibilidade mínima necessária, nada vira `pub`.
|
| 103 |
+
|
| 104 |
+
**Aritmética da divisão**: 4 monólitos (275,2 kB) → 26 arquivos
|
| 105 |
+
(maior submódulo: `python/demos_qwen.rs`; maior arquivo do projeto agora
|
| 106 |
+
`fnh.rs` com 57,6 kB — abaixo do menor monólito criticado).
|
| 107 |
+
|
| 108 |
+
### 2.3 Experimento controlado (refutação da alegação O(N/k)) ∎
|
| 109 |
+
|
| 110 |
+
**Protocolo**: build aquecido; toque único (`os.utime`) no arquivo;
|
| 111 |
+
`cargo build --no-default-features`; mediana de 3 execuções. Grupo de
|
| 112 |
+
controle: `tipos.rs` (arquivo pequeno, inalterado pelo split).
|
| 113 |
+
|
| 114 |
+
| arquivo tocado | v0.9.0 (monólito) | v0.10.0 (submódulo) |
|
| 115 |
+
|---|---|---|
|
| 116 |
+
| jepa (58,4 kB) | 1,06 s | 1,13 s (`jepa/variantes.rs`) |
|
| 117 |
+
| pensamento (53,4 kB) | 1,13 s | 1,15 s (`pensamento/motor.rs`) |
|
| 118 |
+
| moe (50,9 kB) | 1,05 s | 1,20 s (`moe/nucleo.rs`) |
|
| 119 |
+
| **controle** tipos.rs | 1,21 s | 1,19 s |
|
| 120 |
+
|
| 121 |
+
O controle varia tanto quanto os alvos (±0,15 s) — **o custo incremental
|
| 122 |
+
é dominado pelo overhead fixo do rustc/cargo, não pelo tamanho do arquivo
|
| 123 |
+
modificado**. A alegação O(N/k) é refutada nesta granularidade. Builds
|
| 124 |
+
frios (target dir separado): domínio 70→68 s, completo 85→86 s —
|
| 125 |
+
**neutros**; a divisão não degradou nada e o ganho é o SRP.
|
| 126 |
+
|
| 127 |
+
### 2.4 O ganho real de compilação já estava capturado
|
| 128 |
+
|
| 129 |
+
A separação que MUDA o tempo de compilação é a de **features** (v0.9.0,
|
| 130 |
+
r-34 §5): domínio testa sem linkar Python. Overhead Python+ponte medido
|
| 131 |
+
agora: 22,0% (68 s vs 86 s frios) — consistente com os 17,6% da r-34
|
| 132 |
+
(mesmo protocolo, máquina com carga diferente).
|
| 133 |
+
|
| 134 |
+
## §3 — Proposta 1: trait `CamadaKhtst` (polimorfismo de camadas)
|
| 135 |
+
|
| 136 |
+
### 3.1 Análise e erratas da proposta
|
| 137 |
+
|
| 138 |
+
1. **Premissa falsa** (verificada no fonte): `rg "fnh|moe|jepa" src/controlador.rs`
|
| 139 |
+
= **0 ocorrências**. O `Controlador` regula hiperparâmetros
|
| 140 |
+
(AutoAjustePensamento/Ucb1/AutoAjusteSom); ele NÃO conhece camadas
|
| 141 |
+
concretas. O problema que o trait resolveria não existe no sítio
|
| 142 |
+
alegado — mas o trait ainda tem valor como abstração de composição
|
| 143 |
+
(adotado).
|
| 144 |
+
2. **Assinatura quebra o otimizador**: `fn parametros(&self) -> Vec<&Tensor>`
|
| 145 |
+
devolve visões somente-leitura; a partir de `&Tensor` não se recupera a
|
| 146 |
+
`Var` para `set()` in-place ⇒ qualquer otimizador via trait ficaria
|
| 147 |
+
**degradado a cópia-e-devolve** (ou não compila). Corrigido para
|
| 148 |
+
`Vec<Var>`.
|
| 149 |
+
3. **Violação de segregação de interface**: `gate_ativacao` na base força
|
| 150 |
+
TODA camada a ter gate de punição. Movida para o trait separado
|
| 151 |
+
`CamadaHomeostatica`.
|
| 152 |
+
4. **"Prova" mal tipada**: "a composição de funções lineares e ativações
|
| 153 |
+
preserva a dimensionalidade do batch" — falso para D (projeções mudam
|
| 154 |
+
D por definição). O invariante correto é sobre **B×T**, provado como
|
| 155 |
+
Teo CK-1 (em `camadas.rs`): reshape canônico fixa B·T; matmul age à
|
| 156 |
+
direita; ativações elemento-a-elemento. ∎
|
| 157 |
+
5. **Casamento incompleto com a MoE**: `MoeRedirecionavel::forward(x, papel)`
|
| 158 |
+
é parametrizada por papel; forçar `forward(&self, x)` perderia o
|
| 159 |
+
parâmetro (proibido remover). Resolvido com o **adaptador zero-custo**
|
| 160 |
+
`MoeComPapel { moe, papel }` que captura o papel — os caminhos de
|
| 161 |
+
treino seguem usando o forward completo (com perda auxiliar e carga).
|
| 162 |
+
|
| 163 |
+
### 3.2 Implementação (aditiva)
|
| 164 |
+
|
| 165 |
+
`camadas.rs`: `trait CamadaKhtst { forward; parametros }`,
|
| 166 |
+
`trait CamadaHomeostatica { gate_ativacao(&Probabilidade) -> GateAtivacao }`,
|
| 167 |
+
impls para `Linear`, `MlpJepa`, `MoeComPapel`; `fnh.rs`: impl
|
| 168 |
+
`CamadaHomeostatica` para `GatePunicao` (via `valor_tipado`, T-V2).
|
| 169 |
+
|
| 170 |
+
### 3.3 Testes (4 novos, todos verdes)
|
| 171 |
+
|
| 172 |
+
- **ck1a**: composição heterogênea `Vec<&dyn CamadaKhtst>` =
|
| 173 |
+
Linear→MlpJepa sobre (2,3,8): saída (2,3,32) — B×T preservado (CK-1);
|
| 174 |
+
MoE via adaptador sobre (6,32).
|
| 175 |
+
- **ck1b**: `parametros()` não-vazio em todas as impls (otimizável).
|
| 176 |
+
- **ck1c**: adaptador delega **bit-a-bit** (comparação `to_bits()` por
|
| 177 |
+
elemento) para `moe.forward(x, papel).saida`.
|
| 178 |
+
- **ck2a**: despacho homeostático heterogêneo: zona morta exata em
|
| 179 |
+
π=0,2 < θ_π; gate > 0 em π=0,95.
|
| 180 |
+
|
| 181 |
+
## §4 — Crítica 3 + Proposta 3: checkpoint atômico com prova de integridade
|
| 182 |
+
|
| 183 |
+
### 4.1 Análise da crítica — VÁLIDA
|
| 184 |
+
|
| 185 |
+
O padrão v0.9.0 era `fs::write(tmp)` + `rename` **sem fsync** e **sem
|
| 186 |
+
verificação de integridade**; o Python decidia resume por
|
| 187 |
+
`os.path.exists`. Janelas reais de falha: (a) queda de energia após o
|
| 188 |
+
rename mas antes do flush dos dados ⇒ arquivo final existe com conteúdo
|
| 189 |
+
zero/parcial (metadata journalizada, dados não — comportamento
|
| 190 |
+
dependente do FS); (b) TOCTOU entre `exists()` e o carregamento;
|
| 191 |
+
(c) corrupção silenciosa (bit rot, truncamento por ENOSPC parcial)
|
| 192 |
+
indetectável — o carregador legado só falha SE o JSON quebrar, e um
|
| 193 |
+
checkpoint truncado no meio de um array passa despercebido até o
|
| 194 |
+
`dados.len() != produto(forma)` — que existia, mas sem hash não detecta
|
| 195 |
+
substituição de mesmo formato.
|
| 196 |
+
|
| 197 |
+
### 4.2 Erratas da proposta (3)
|
| 198 |
+
|
| 199 |
+
1. **Destruição do estado anterior**: `File::create(caminho_pesos)`
|
| 200 |
+
trunca o checkpoint VÁLIDO anterior ANTES de escrever o novo. Queda
|
| 201 |
+
de energia no meio da escrita = perda dos DOIS estados. A atomicidade
|
| 202 |
+
real exige tmp+rename (a v0.9.0 JÁ fazia tmp+rename; a proposta seria
|
| 203 |
+
um REGRESSO).
|
| 204 |
+
2. **Manifesto sem barreira**: `fs::write(caminho_meta, ...)` sem
|
| 205 |
+
`sync_all` — o manifesto pode não atingir o disco quando os pesos já
|
| 206 |
+
atingiram, violando a própria ordenação que a "prova de atomicidade"
|
| 207 |
+
alegada depende.
|
| 208 |
+
3. **`bincode::serialize(...).unwrap()`** em caminho de I/O + dependência
|
| 209 |
+
nova para serializar `&[f32]` que o projeto já serializa em JSON;
|
| 210 |
+
crates.io inacessível no ambiente ⇒ dependência adicional
|
| 211 |
+
inaceitável. **SHA-256 implementado em Rust puro** (FIPS 180-4,
|
| 212 |
+
~90 linhas) com os vetores oficiais como teste (cp0a).
|
| 213 |
+
|
| 214 |
+
**Errata de linguagem**: "Se M existe e H(W) == M.hash, então W é
|
| 215 |
+
matematicamente idêntico ao salvo" — exagero; o correto é
|
| 216 |
+
**indistinguibilidade computacional** com P[colisão] ≤ 2⁻¹²⁸ (limite de
|
| 217 |
+
aniversário, CP-3). Truncamento é capturado DETERMINISTICAMENTE pelo
|
| 218 |
+
campo `tamanho`.
|
| 219 |
+
|
| 220 |
+
### 4.3 O que foi implementado (Teos CP-1, CP-2, CP-3 em `checkpoint.rs`)
|
| 221 |
+
|
| 222 |
+
Ordem de durabilidade: `write(W.tmp) → fsync(W.tmp) → rename(W) →
|
| 223 |
+
write(M.tmp) → fsync(M.tmp) → rename(M) → fsync(dir)`. Manifesto
|
| 224 |
+
lado-a-lado `{ck}.manifest.json` = `{versao, algoritmo, hash_sha256,
|
| 225 |
+
tamanho}`. API: `salvar_atomico`, `carregar_verificado` (Err se
|
| 226 |
+
manifesto ausente/divergente), `validar` (bool p/ Python),
|
| 227 |
+
`sha256_hex`. Integrado em `gerador.salvar`, `tokenizador.salvar`,
|
| 228 |
+
`Bpe.salvar` (python) — **os carregadores legados permanecem**
|
| 229 |
+
(compatibilidade v0.9.0; nada removido); acrescentados
|
| 230 |
+
`carregar_verificado` (gerador/tokenizador) e `carregar` nativo do
|
| 231 |
+
tokenizador (antes só existia no helper Python).
|
| 232 |
+
|
| 233 |
+
**Custo**: manifesto de 140–141 B por checkpoint (overhead 5,8e-7 do
|
| 234 |
+
checkpoint de 240,9 MB); SHA-256 ≈ 1,1 s/240 MB em release single-thread
|
| 235 |
+
(medido dentro do salvar; amornado pelo próprio treino).
|
| 236 |
+
|
| 237 |
+
### 4.4 Testes (6 novos + drill Python)
|
| 238 |
+
|
| 239 |
+
cp0a (vetores FIPS: vazio, "abc", 3-blocos), cp1a (roundtrip + tmp
|
| 240 |
+
limpo), cp1b (**falha simulada** — dir somente-leitura ⇒ Err e o
|
| 241 |
+
checkpoint ANTERIOR permanece válido), cp3a (adulteração 1 byte),
|
| 242 |
+
cp3b (truncamento), cp4a (manifesto ausente recusado), cp5a (manifesto
|
| 243 |
+
de outro arquivo recusado). **Drill Python** (política de resume do CLI):
|
| 244 |
+
íntegro→"válido"; adulterado→"corrompido (apagado)"; legado→reuso com
|
| 245 |
+
detecção por parse.
|
| 246 |
+
|
| 247 |
+
## §5 — Validação com dataset real (Wikipédia PT-BR)
|
| 248 |
+
|
| 249 |
+
Pipeline A/B/C fresco na pilha v0.10.0 (1.121 sequências ctx=256,
|
| 250 |
+
`wikimedia/wikipedia 20231101.pt`, streaming controlado; o
|
| 251 |
+
`telemetria_fase_c.jsonl` residual da v0.9.0 foi apagado antes — §1):
|
| 252 |
+
|
| 253 |
+
| fase | antes | depois | custo | observação |
|
| 254 |
+
|---|---|---|---|---|
|
| 255 |
+
| A (tokenizador GSQ+RCO) | 9,8818 | **8,7258** | 449 s | acc_recon 0,123; **δ_ort 1,01e-06** (ortogonalidade exata em treino real) |
|
| 256 |
+
| B (gerador NCM) | 4,1010 | **3,8219** | 266 s | abaixo do acaso ln 64 = 4,159 (aprendizado real) |
|
| 257 |
+
| C (conjunto, p/ FNH) | 3,7730 | **3,7508** | cap 400 s | δ_ort 2,0e-07; π(C)=0,000 ⇒ gate corretamente FECHADO (homeostase: punição saudável) |
|
| 258 |
+
|
| 259 |
+
Checkpoints verificados pós-treino: tokenizador 240.853.123 B
|
| 260 |
+
(sha256 `202e9d9fbeba8551…`, válido), gerador 59.527.584 B
|
| 261 |
+
(`74a7a4595b1b4b20…`, válido). Resume subsequente via CLI:
|
| 262 |
+
"válido → reutilizando" (as fases A/B não retreinaram — prova
|
| 263 |
+
criptográfica de que o estado em disco é o salvo).
|
| 264 |
+
|
| 265 |
+
**Leitura honesta**: as magnitudes de perda são comparáveis às da v0.9.0
|
| 266 |
+
(CE C ≈ 3,75–3,93 dependendo do lote; confiança absoluta segue na casa
|
| 267 |
+
0,01–0,02, o débito de 10–50× compute das r-07/r-27/r-32 permanece). O
|
| 268 |
+
que a v0.10.0 PROVA é a **integridade e a reprodutibilidade do estado**
|
| 269 |
+
— o que a crítica 3 pedia — sem regressão de aprendizado.
|
| 270 |
+
|
| 271 |
+
## §6 — Crítica 2 + Proposta 4: CLI unificado (MLOps)
|
| 272 |
+
|
| 273 |
+
**Análise**: a pasta `scripts/` tem 36 arquivos numerados — factualmente
|
| 274 |
+
correto. A proposta "substituir por um CLI declarativo" foi **rejeitada
|
| 275 |
+
nos termos**: (a) os scripts numerados são a **trilha de rastreabilidade**
|
| 276 |
+
das memórias (r-04 cita "scripts 39-43", r-22 cita "48-54", r-27 "59",
|
| 277 |
+
r-32 "61-66", r-33 "68-69", r-34 "70-74"); (b) a regra da casa proíbe
|
| 278 |
+
remover funcionalidade eficaz; (c) o exemplo de código da proposta
|
| 279 |
+
referencia `K.Config`/`K.treinar_fase_a` (corretos por coincidência) mas
|
| 280 |
+
inventa `validar_integridade` sem fonte e fixa `fase 1|2|3` — o pipeline
|
| 281 |
+
real tem fases A/B/C com parâmetros distintos.
|
| 282 |
+
|
| 283 |
+
**Implementação (aditiva)**: `scripts/75_cli.py` (Typer 0.23):
|
| 284 |
+
`versao`, `coletar`, `treinar` (com pré-validação SHA-256 dos
|
| 285 |
+
checkpoints — política §4.4), `verificar-checkpoints` (relatório),
|
| 286 |
+
`suite`, `telemetria`, `publicar` (exige `--confirmar` + `HF_TOKEN` no
|
| 287 |
+
ambiente), `estados` (varre/apaga estados locais). Delegação por
|
| 288 |
+
subprocess aos scripts canônicos — **zero duplicação de lógica**.
|
| 289 |
+
|
| 290 |
+
## §7 — Autocrítica (minha)
|
| 291 |
+
|
| 292 |
+
1. **O split quase reintroduziu o problema que criticava**: a 1ª
|
| 293 |
+
tentativa criou `python/{jepa, pensamento, verificador}.rs` — nomes
|
| 294 |
+
que SOMBREIAM `crate::{jepa, pensamento, verificador}` via
|
| 295 |
+
`use super::*` + `use crate::*`. Detectado pelos erros E0425/E0433,
|
| 296 |
+
corrigido com renomes (`jepa_treinos`, `pensamento_demos`,
|
| 297 |
+
`calibracao`). Lição: divisão por domínio exige auditoria de colisão
|
| 298 |
+
de nomes com a raiz do crate.
|
| 299 |
+
2. **Dois passos de edição quebrados meus** (stub vazio em
|
| 300 |
+
`carregar_de_txt`; substituição que apagou `carregar_verificado`
|
| 301 |
+
em vez de precedê-lo) — ambos capturados pelo compilador antes de
|
| 302 |
+
qualquer teste/publicação; o corpo original do `tokenizador::carregar`
|
| 303 |
+
foi recuperado do git (não existia — era helper Python; reimplementado
|
| 304 |
+
nativo como ADIÇÃO).
|
| 305 |
+
3. **O drill de corrupção falhou na 1ª execução por erro MEU** no cenário
|
| 306 |
+
(manifesto com `tamanho: 15` para 16 bytes) — a função agiu
|
| 307 |
+
CORRETAMENTE (tamanho divergente ⇒ corrompido). O "fracasso" do
|
| 308 |
+
teste era o teste funcionando; documentado aqui porque um leitor
|
| 309 |
+
apressado teria "corrigido" a função errada.
|
| 310 |
+
4. **fnh.rs (57,6 kB) agora é o maior arquivo** — próximo do menor
|
| 311 |
+
monólito criticado (50,9 kB). Não dividido nesta versão: suas
|
| 312 |
+
responsabilidades (rotações/cayley/suavizador/gate/camada) são
|
| 313 |
+
coesas sob o domínio FNH e a divisão adicionaria acoplamento
|
| 314 |
+
artificial; registrado como débito de observação.
|
| 315 |
+
5. **A medição incremental tem ruído da máquina** (±0,15 s) — por isso o
|
| 316 |
+
grupo de controle; a conclusão (refutação de O(N/k) em granularidade
|
| 317 |
+
de arquivo) é robusta ao ruído porque o EFEITO alegado seria de
|
| 318 |
+
ordens de magnitude, não de 15%.
|
| 319 |
+
6. **`versao()` estava fixada em "0.9.0"** — corrigida para derivar de
|
| 320 |
+
`CARGO_PKG_VERSION` (o build antigo mentia a própria versão; um
|
| 321 |
+
bug meu de versões anteriores que a telemetria v0.10.0 expôs).
|
| 322 |
+
|
| 323 |
+
## §8 — Débitos honestos remanescentes
|
| 324 |
+
|
| 325 |
+
- **Divisão em crates (workspace)** para o ganho O(N/k) real de
|
| 326 |
+
compilação: especificado em CK-0, não executado (custo de migração
|
| 327 |
+
PyO3/feature-unification alto; benefício atual pequeno — builds frios
|
| 328 |
+
de 68–86 s não justificam).
|
| 329 |
+
- **fsync de diretório é Linux-only** (`#[cfg(unix)]`); em plataformas
|
| 330 |
+
sem fsync de dir aberto, a durabilidade do rename fica a cargo do SO
|
| 331 |
+
(documentado no fonte).
|
| 332 |
+
- **SHA-256 single-thread** (~1,1 s/240 MB): paralelizável por blocos se
|
| 333 |
+
os checkpoints crescerem de ordem de magnitude.
|
| 334 |
+
- **Telemetria jsonl da fase C** continua append sem manifesto — é log,
|
| 335 |
+
não estado; o resume tolera linha parcial (parse por linha) mas não a
|
| 336 |
+
valida por hash (extensão: rotacionar com hash por segmento).
|
| 337 |
+
- **Kani** para os newtypes: mesmo débito justificado da r-34 §6.
|
| 338 |
+
- **Confiança absoluta 0,01–0,02** segue exigindo 10–50× compute
|
| 339 |
+
(herdado); a v0.10.0 garante a INTEGRIDADE do estado que se tem, não
|
| 340 |
+
a magnitude do aprendizado.
|
| 341 |
+
|
| 342 |
+
## §9 — Rastreabilidade (afirmação → decisão → código)
|
| 343 |
+
|
| 344 |
+
| prova | sítio | teste |
|
| 345 |
+
|---|---|---|
|
| 346 |
+
| CK-0 (granularidade) | r-35 §2.1/§2.4 | medições §2.3 (telemetria_v010.json.compilacao) |
|
| 347 |
+
| CK-1 (composição B×T) | camadas.rs (doc do trait) | ck1a |
|
| 348 |
+
| CP-1 (troca atômica) | checkpoint.rs | cp1a, cp1b |
|
| 349 |
+
| CP-2 (durabilidade ordenada) | checkpoint.rs | cp1a + ordem no código |
|
| 350 |
+
| CP-3 (detecção) | checkpoint.rs | cp0a, cp3a, cp3b, cp5a |
|
| 351 |
+
| T-V3 (γ ∈ (0,∞) por construção) | tipos.rs | tv3a/b/c |
|
| 352 |
+
| T-V3 fronteira verificador | verificador.rs | tv3d, tv3e |
|
| 353 |
+
| MoeComPapel bit-a-bit | camadas.rs | ck1c |
|
| 354 |
+
| política de resume | scripts/75_cli.py | drill §4.4 + treino real §5 |
|
| 355 |
+
|
| 356 |
+
**Gates consolidados (script 77)**: 107/107 = 97 herdados v0.9.0 + 10
|
| 357 |
+
novos (s1×3, s2×1[2 checkpoints], s3s4×1, s5×4, s6×1). Cargo 123/123
|
| 358 |
+
(107 herdados + 16 novos). Determinismo preservado pela construção
|
| 359 |
+
aditiva (nenhum caminho existente alterado semanticamente; oráculos
|
| 360 |
+
legados intactos).
|
pyproject.toml
CHANGED
|
@@ -4,8 +4,8 @@ build-backend = "maturin"
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "khtst-rust"
|
| 7 |
-
version = "0.
|
| 8 |
-
description = "KHTST_RUST —
|
| 9 |
requires-python = ">=3.10"
|
| 10 |
license = { text = "Apache-2.0" }
|
| 11 |
|
|
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "khtst-rust"
|
| 7 |
+
version = "0.10.0"
|
| 8 |
+
description = "KHTST_RUST — v0.10.0: SRP nos módulos, checkpoints atômicos com prova criptográfica, tipos de domínio com invariantes, polimorfismo de camadas e CLI MLOps unificada (críticas Qwen3.8 lote 3 analisadas/provadas/testadas em r-35)"
|
| 9 |
requires-python = ">=3.10"
|
| 10 |
license = { text = "Apache-2.0" }
|
| 11 |
|
resultados/fase_a_v010.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fase": "a",
|
| 3 |
+
"resumo": {
|
| 4 |
+
"acc_recon": 0.123046875,
|
| 5 |
+
"delta_ort_max": 1.012120244413075e-06,
|
| 6 |
+
"escore_aprendizado": 0.34142467641580315,
|
| 7 |
+
"monotonia_prefixo": 0.5,
|
| 8 |
+
"passos": 400,
|
| 9 |
+
"perda_final": 8.72581958770752,
|
| 10 |
+
"perda_inicial": 9.88181209564209,
|
| 11 |
+
"perplexidade_codebook": 54.26808964591084
|
| 12 |
+
},
|
| 13 |
+
"tempo_s": 449.4470684528351
|
| 14 |
+
}
|
resultados/fase_b_v010.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fase": "b",
|
| 3 |
+
"resumo": {
|
| 4 |
+
"acc_conceitos": 0.10212053571428571,
|
| 5 |
+
"escore_aprendizado": 0.19593913663076906,
|
| 6 |
+
"passos": 400,
|
| 7 |
+
"perda_final": 3.8218880721500943,
|
| 8 |
+
"perda_inicial": 4.1010067122323175
|
| 9 |
+
},
|
| 10 |
+
"tempo_s": 265.64751839637756
|
| 11 |
+
}
|
resultados/suite_v010_final.json
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"versao": "0.10.0",
|
| 3 |
+
"blocos": {
|
| 4 |
+
"legado_v090": "97/97",
|
| 5 |
+
"s1_divisao_srp": "3/3",
|
| 6 |
+
"s2_checkpoint_atomico": "2/2",
|
| 7 |
+
"s3s4_tipos_trait": "1/1",
|
| 8 |
+
"s5_treino_real": "4/4",
|
| 9 |
+
"s6_cli": "1/1"
|
| 10 |
+
},
|
| 11 |
+
"gates": {
|
| 12 |
+
"legado_legado_legado_legado_calibracao_soundness.v1_calibracao": true,
|
| 13 |
+
"legado_legado_legado_legado_calibracao_soundness.v2_soundness": true,
|
| 14 |
+
"legado_legado_legado_legado_calibracao_soundness.v3_tau_positivo": true,
|
| 15 |
+
"legado_legado_legado_legado_ciclo_d_sem_oraculo.aprendeu": true,
|
| 16 |
+
"legado_legado_legado_legado_ciclo_d_sem_oraculo.terminou": true,
|
| 17 |
+
"legado_legado_legado_legado_ciclo_d_sem_oraculo.sem_oraculo": true,
|
| 18 |
+
"legado_legado_legado_legado_ciclo_d_sem_oraculo.calibrado": true,
|
| 19 |
+
"legado_legado_legado_legado_ciclo_d_sem_oraculo.ganho_nao_negativo": true,
|
| 20 |
+
"legado_legado_legado_legado_ciclo_d_sem_oraculo.verificador_sem_oraculo": true,
|
| 21 |
+
"legado_legado_legado_legado_escalonamento_n.tp2_ncm": true,
|
| 22 |
+
"legado_legado_legado_legado_fase_c.verificador_sem_oraculo": true,
|
| 23 |
+
"legado_legado_legado_legado_fase_c.calibracao_nll_otima": true,
|
| 24 |
+
"legado_legado_legado_legado_fase_c.ece_aceitavel_para_soundness": true,
|
| 25 |
+
"legado_legado_legado_legado_fase_c.correcao_ativa": true,
|
| 26 |
+
"legado_legado_legado_legado_fase_c.acoplador_sem_timeout": true,
|
| 27 |
+
"legado_legado_legado_legado_fase_c.acoplador_duplex_ativo": true,
|
| 28 |
+
"legado_legado_legado_legado_fase_c.confianca_pos_nao_inferior": true,
|
| 29 |
+
"legado_legado_legado_legado_fase_c.backbone_preservado": true,
|
| 30 |
+
"legado_legado_legado_legado_fase_c.pcgrad_sem_conflito_observado": true,
|
| 31 |
+
"legado_legado_legado_legado_provas_mr.mr1_capacidade": true,
|
| 32 |
+
"legado_legado_legado_legado_provas_mr.mr1_perda_aux": true,
|
| 33 |
+
"legado_legado_legado_legado_provas_mr.mr2_neutralidade": true,
|
| 34 |
+
"legado_legado_legado_legado_provas_mr.mr3_paralelismo": true,
|
| 35 |
+
"legado_legado_legado_legado_provas_mr.mr5_water_filling": true,
|
| 36 |
+
"legado_legado_legado_legado_jepa_moe.jepa_moe_aprende": true,
|
| 37 |
+
"legado_legado_legado_legado_jepa_moe.jepa_moe_vs_densa": true,
|
| 38 |
+
"legado_legado_legado_legado_jepa_moe.jepa_moe_vs_pequena": true,
|
| 39 |
+
"legado_legado_legado_legado_som_moe.moe_refina": true,
|
| 40 |
+
"legado_legado_legado_legado_som_moe.redirect_decodifica": true,
|
| 41 |
+
"legado_legado_legado_legado_som_moe.wf_melhor_uniforme": true,
|
| 42 |
+
"legado_legado_legado_legado_acoplador_duplo.duplex_simultaneo": true,
|
| 43 |
+
"legado_legado_legado_legado_acoplador_duplo.sem_timeout": true,
|
| 44 |
+
"legado_legado_legado_legado_acoplador_duplo.terminou": true,
|
| 45 |
+
"legado_legado_legado_legado_iter.gate_ucb1_encontrou_moe": true,
|
| 46 |
+
"legado_legado_legado_legado_iter.gate_gap_positivo_vs_baseline": true,
|
| 47 |
+
"legado_legado_legado_legado_iter.gate_arrependimento_pequeno": true,
|
| 48 |
+
"legado_legado_legado_legado_iter.gate_regulacao_ocorreu": true,
|
| 49 |
+
"legado_legado_legado_legado_iter.gate_parada_ou_teto": true,
|
| 50 |
+
"legado_legado_legado_legado_iter.gate_melhor_aprendeu": true,
|
| 51 |
+
"legado_legado_legado_legado_iter.a1_tau_quantil": true,
|
| 52 |
+
"legado_legado_legado_legado_iter.a2_b_finito": true,
|
| 53 |
+
"legado_legado_legado_legado_iter.a3_n_reduzido": true,
|
| 54 |
+
"legado_legado_legado_legado_iter.a4_ucb1_achou": true,
|
| 55 |
+
"legado_legado_legado_legado_iter.a5_gate_subiu": true,
|
| 56 |
+
"legado_legado_legado_legado_legada_suite_v": true,
|
| 57 |
+
"legado_legado_legado_legado_cargo_54": true,
|
| 58 |
+
"legado_legado_legado_fnh_f1_so_n_exato": true,
|
| 59 |
+
"legado_legado_legado_fnh_f3_gate_zona_morta": true,
|
| 60 |
+
"legado_legado_legado_fnh_f3_gate_abre": true,
|
| 61 |
+
"legado_legado_legado_fnh_f4_homeostase_theta_congelado": true,
|
| 62 |
+
"legado_legado_legado_fnh_f4_rotacao_aprende_com_punicao": true,
|
| 63 |
+
"legado_legado_legado_fnh_a2_delta_ort_treino_real": true,
|
| 64 |
+
"legado_legado_legado_fnh_a3_punicao_real_medida": true,
|
| 65 |
+
"legado_legado_legado_arvore_quebra_t1_invariantes": true,
|
| 66 |
+
"legado_legado_legado_arvore_quebra_t2_agregacao_exata": true,
|
| 67 |
+
"legado_legado_legado_arvore_quebra_t3_teto_simetrico": true,
|
| 68 |
+
"legado_legado_legado_arvore_quebra_q1_executa_real": true,
|
| 69 |
+
"legado_legado_legado_arvore_quebra_q2_soundness_aceites": true,
|
| 70 |
+
"legado_legado_legado_arvore_quebra_q3_invariantes_pos_quebra": true,
|
| 71 |
+
"legado_legado_legado_arvore_quebra_q4_punicao_registrada": true,
|
| 72 |
+
"legado_legado_legado_arvore_quebra_q5_sintetico_mistura_aceita_rejeita": true,
|
| 73 |
+
"legado_legado_legado_nlp_nlg_n_coerencia_ok": true,
|
| 74 |
+
"legado_legado_legado_nlp_nlg_n_espec_ok": true,
|
| 75 |
+
"legado_legado_legado_nlp_nlg_n_gate_medio_ok": true,
|
| 76 |
+
"legado_legado_legado_nlp_nlg_n_nlg_neutro": true,
|
| 77 |
+
"legado_legado_legado_nlp_nlg_n_nlp_neutro": true,
|
| 78 |
+
"legado_legado_legado_nlp_nlg_n_ctx_real_bigramas_unicos": true,
|
| 79 |
+
"legado_legado_legado_nlp_nlg_s_piso_ok": true,
|
| 80 |
+
"legado_legado_legado_nlp_nlg_s_qe_sobe": true,
|
| 81 |
+
"legado_legado_legado_nlp_nlg_s_stop_dispara": true,
|
| 82 |
+
"legado_legado_legado_nlp_nlg_s_tetos_ok": true,
|
| 83 |
+
"legado_legado_legado_telemetria_painel_v070": true,
|
| 84 |
+
"legado_legado_legado_treino_real_completo": true,
|
| 85 |
+
"legado_legado_q1_newton_menos_avaliacoes": true,
|
| 86 |
+
"legado_legado_q1_newton_mesmo_t": true,
|
| 87 |
+
"legado_legado_q1_newton_nll_nao_piora": true,
|
| 88 |
+
"legado_legado_q3_cayley_ambos_exatos": true,
|
| 89 |
+
"legado_legado_q3_cayley_cayley_mais_rapido": true,
|
| 90 |
+
"legado_legado_q2_arvore_iguais_bit_a_bit": true,
|
| 91 |
+
"legado_legado_q4_moe_bug_v080_1_confirmado": true,
|
| 92 |
+
"legado_legado_q4_moe_gradiente_restaurado": true,
|
| 93 |
+
"legado_legado_q4_moe_valores_iguais": true,
|
| 94 |
+
"legado_legado_q5_ablacao_moe_aprende_3x": true,
|
| 95 |
+
"legado_legado_q5_ablacao_moe_domina_pequena_3x": true,
|
| 96 |
+
"legado_legado_cargo_todos_verdes": true,
|
| 97 |
+
"legado_q1_skew_bits_identicos": true,
|
| 98 |
+
"legado_q1_skew_mais_rapido_n128": true,
|
| 99 |
+
"legado_q2_calibrar_bit_a_bit": true,
|
| 100 |
+
"legado_q2_calibrar_nao_regride": true,
|
| 101 |
+
"legado_q3_forward_valores_e_gradiente": true,
|
| 102 |
+
"legado_q4_newtypes_fronteira_validada": true,
|
| 103 |
+
"legado_real_gate_abre_com_punicao_alta": true,
|
| 104 |
+
"legado_real_gate_fecha_com_punicao_baixa": true,
|
| 105 |
+
"legado_real_rotacao_aprende": true,
|
| 106 |
+
"legado_real_so_n_exato": true,
|
| 107 |
+
"legado_real_ce_decresce": true,
|
| 108 |
+
"legado_q5_compila_sem_python": true,
|
| 109 |
+
"s1_nenhum_arquivo_acima_60kB": true,
|
| 110 |
+
"s1_monolitos_dissolvidos": true,
|
| 111 |
+
"s1_testes_herdados_preservados": true,
|
| 112 |
+
"s2_checkpoints_sha256_validos": true,
|
| 113 |
+
"s3s4_testes_novos_16": true,
|
| 114 |
+
"s5a_faseA_perda_cai": true,
|
| 115 |
+
"s5b_faseB_perda_cai": true,
|
| 116 |
+
"s5c_faseC_ce_cai": true,
|
| 117 |
+
"s5d_ortogonalidade_exata": true,
|
| 118 |
+
"s6_cli_comandos": true
|
| 119 |
+
},
|
| 120 |
+
"medicoes": {
|
| 121 |
+
"maior_arquivo": {
|
| 122 |
+
"caminho": "src/fnh.rs",
|
| 123 |
+
"kB": 57.6
|
| 124 |
+
},
|
| 125 |
+
"cargo_total": 123,
|
| 126 |
+
"checkpoints": {
|
| 127 |
+
"tokenizador_conceitos.json": {
|
| 128 |
+
"valido": true,
|
| 129 |
+
"manifesto_bytes": 141
|
| 130 |
+
},
|
| 131 |
+
"gerador_conceitos.json": {
|
| 132 |
+
"valido": true,
|
| 133 |
+
"manifesto_bytes": 140
|
| 134 |
+
}
|
| 135 |
+
},
|
| 136 |
+
"testes_novos_v010": 16,
|
| 137 |
+
"treino_real": {
|
| 138 |
+
"a": {
|
| 139 |
+
"perda_inicial": 9.88181209564209,
|
| 140 |
+
"perda_final": 8.72581958770752,
|
| 141 |
+
"delta_ort_max": 1.012120244413075e-06
|
| 142 |
+
},
|
| 143 |
+
"b": {
|
| 144 |
+
"perda_inicial": 4.1010067122323175,
|
| 145 |
+
"perda_final": 3.8218880721500943
|
| 146 |
+
},
|
| 147 |
+
"c": {
|
| 148 |
+
"ce_inicial": 3.773000853402274,
|
| 149 |
+
"ce_final": 3.750825984137399,
|
| 150 |
+
"taxa_gatilho": null
|
| 151 |
+
}
|
| 152 |
+
},
|
| 153 |
+
"cli": {
|
| 154 |
+
"versao": "khtst-rust-0.10.0",
|
| 155 |
+
"checkpoints_relacionados": 2
|
| 156 |
+
}
|
| 157 |
+
},
|
| 158 |
+
"veredito": "107/107 — APROVADO"
|
| 159 |
+
}
|
resultados/telemetria_fase_c.jsonl
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
resultados/telemetria_v010.json
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"versao": "0.10.0",
|
| 3 |
+
"estrutura": {
|
| 4 |
+
"arquivos_fonte": 61,
|
| 5 |
+
"maior_arquivo": {
|
| 6 |
+
"caminho": "src/fnh.rs",
|
| 7 |
+
"bytes": 57580
|
| 8 |
+
},
|
| 9 |
+
"monolitos_v090_bytes": {
|
| 10 |
+
"src/python.rs": 112410,
|
| 11 |
+
"src/jepa.rs": 58407,
|
| 12 |
+
"src/pensamento.rs": 53408,
|
| 13 |
+
"src/moe_redirecionavel.rs": 50941
|
| 14 |
+
},
|
| 15 |
+
"subdirs": [
|
| 16 |
+
"src/jepa",
|
| 17 |
+
"src/moe_redirecionavel",
|
| 18 |
+
"src/pensamento",
|
| 19 |
+
"src/python"
|
| 20 |
+
],
|
| 21 |
+
"arquivos_por_diretorio": {
|
| 22 |
+
"src": 31,
|
| 23 |
+
"src/jepa": 8,
|
| 24 |
+
"src/moe_redirecionavel": 6,
|
| 25 |
+
"src/pensamento": 5,
|
| 26 |
+
"src/python": 11
|
| 27 |
+
}
|
| 28 |
+
},
|
| 29 |
+
"compilacao": {
|
| 30 |
+
"frio_dominio_s": 68,
|
| 31 |
+
"frio_completo_s": 86,
|
| 32 |
+
"frio_dominio_v090_s": 70,
|
| 33 |
+
"frio_completo_v090_s": 85,
|
| 34 |
+
"overhead_python_pct": 20.9,
|
| 35 |
+
"incremental_toque_s": {
|
| 36 |
+
"v090_jepa_rs_monolito": 1.06,
|
| 37 |
+
"v010_jepa_variantes": 1.13,
|
| 38 |
+
"v090_pensamento_rs_monolito": 1.13,
|
| 39 |
+
"v010_pensamento_motor": 1.15,
|
| 40 |
+
"v090_moe_rs_monolito": 1.05,
|
| 41 |
+
"v010_moe_nucleo": 1.2,
|
| 42 |
+
"controle_tipos_rs_v090": 1.21,
|
| 43 |
+
"controle_tipos_rs_v010": 1.19
|
| 44 |
+
},
|
| 45 |
+
"nota": "a alegação O(N/k) da crítica 1 é REFUTADA em granularidade de arquivo: o custo incremental (~1,1s) é dominado pelo overhead fixo do rustc/cargo, não pelo tamanho do arquivo tocado (controle tipos.rs varia igual). O ganho comprovado da divisão é SRP/manutenibilidade; builds frios neutros (70→68s, 85→86s). Ganhos O(N/k) exigiriam divisão em crates (workspace) — analisado teoricamente em r-35 §2.4."
|
| 46 |
+
},
|
| 47 |
+
"testes": {
|
| 48 |
+
"cargo_total": 123,
|
| 49 |
+
"herdados_v090": 107,
|
| 50 |
+
"novos_v010": 16,
|
| 51 |
+
"novos": [
|
| 52 |
+
"cp0a_sha256_vetores_fips",
|
| 53 |
+
"cp1a_salvar_carregar_roundtrip",
|
| 54 |
+
"cp1b_falha_preserva_anterior",
|
| 55 |
+
"cp3a_adulteracao_detectada",
|
| 56 |
+
"cp3b_truncamento_detectado",
|
| 57 |
+
"cp4a_manifesto_ausente_recusado",
|
| 58 |
+
"cp5a_manifesto_de_outro_recusado",
|
| 59 |
+
"tv3a_construtores_recusam_fora_do_dominio",
|
| 60 |
+
"tv3b_fechamento_inversao",
|
| 61 |
+
"tv3c_de_gamma_concorda",
|
| 62 |
+
"tv3d_nll_tipada_equivalente",
|
| 63 |
+
"tv3e_verificador_fronteira",
|
| 64 |
+
"ck1a_composicao_heterogenea_preserva_bt",
|
| 65 |
+
"ck1b_parametros_nao_vazios",
|
| 66 |
+
"ck1c_adaptador_bit_a_bit",
|
| 67 |
+
"ck2a_despacho_homeostatico"
|
| 68 |
+
],
|
| 69 |
+
"saida": "ok"
|
| 70 |
+
},
|
| 71 |
+
"checkpoints": {
|
| 72 |
+
"tokenizador_conceitos.json": {
|
| 73 |
+
"existe": true,
|
| 74 |
+
"valido_sha256": true,
|
| 75 |
+
"bytes": 240853123,
|
| 76 |
+
"manifesto_bytes": 141,
|
| 77 |
+
"hash_prefixo": "202e9d9fbeba8551…"
|
| 78 |
+
},
|
| 79 |
+
"gerador_conceitos.json": {
|
| 80 |
+
"existe": true,
|
| 81 |
+
"valido_sha256": true,
|
| 82 |
+
"bytes": 59527584,
|
| 83 |
+
"manifesto_bytes": 140,
|
| 84 |
+
"hash_prefixo": "74a7a4595b1b4b20…"
|
| 85 |
+
}
|
| 86 |
+
},
|
| 87 |
+
"treino_real": {
|
| 88 |
+
"a": {
|
| 89 |
+
"perda_inicial": 9.88181209564209,
|
| 90 |
+
"perda_final": 8.72581958770752,
|
| 91 |
+
"tempo_s": 449.4
|
| 92 |
+
},
|
| 93 |
+
"b": {
|
| 94 |
+
"perda_inicial": 4.1010067122323175,
|
| 95 |
+
"perda_final": 3.8218880721500943,
|
| 96 |
+
"tempo_s": 265.6
|
| 97 |
+
},
|
| 98 |
+
"c": {
|
| 99 |
+
"ce_inicial": 3.773000853402274,
|
| 100 |
+
"ce_final": 3.750825984137399,
|
| 101 |
+
"taxa_gatilho_media": null,
|
| 102 |
+
"ece_final": null
|
| 103 |
+
}
|
| 104 |
+
},
|
| 105 |
+
"modulos": [
|
| 106 |
+
{
|
| 107 |
+
"caminho": "src/fnh.rs",
|
| 108 |
+
"kB": 57.6
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"caminho": "src/treino.rs",
|
| 112 |
+
"kB": 47.0
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"caminho": "src/som4d.rs",
|
| 116 |
+
"kB": 37.2
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"caminho": "src/verificador.rs",
|
| 120 |
+
"kB": 28.2
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"caminho": "src/arvore_pesos.rs",
|
| 124 |
+
"kB": 27.8
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"caminho": "src/controlador.rs",
|
| 128 |
+
"kB": 25.3
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"caminho": "src/pensamento/motor.rs",
|
| 132 |
+
"kB": 24.8
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"caminho": "src/tokenizador.rs",
|
| 136 |
+
"kB": 20.9
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"caminho": "src/python/pensamento_demos.rs",
|
| 140 |
+
"kB": 19.3
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"caminho": "src/moe_redirecionavel/nucleo.rs",
|
| 144 |
+
"kB": 18.8
|
| 145 |
+
},
|
| 146 |
+
{
|
| 147 |
+
"caminho": "src/telemetria.rs",
|
| 148 |
+
"kB": 18.4
|
| 149 |
+
},
|
| 150 |
+
{
|
| 151 |
+
"caminho": "src/nlg.rs",
|
| 152 |
+
"kB": 18.3
|
| 153 |
+
}
|
| 154 |
+
]
|
| 155 |
+
}
|
resultados/treino_v090.json
CHANGED
|
@@ -1,33 +1,33 @@
|
|
| 1 |
{
|
| 2 |
"versao": "0.9.0",
|
| 3 |
-
"build": "khtst-rust-0.
|
| 4 |
"dados": {
|
| 5 |
"sequencias": 1121,
|
| 6 |
"fonte": "wikimedia/wikipedia 20231101.pt"
|
| 7 |
},
|
| 8 |
"fases": {
|
| 9 |
"c": {
|
| 10 |
-
"acordos_medios": 0.
|
| 11 |
-
"ce_final": 3.
|
| 12 |
-
"ce_gatilhada_com_correcao": 4.
|
| 13 |
-
"ce_gatilhada_sem_correcao": 4.
|
| 14 |
-
"ce_inicial": 3.
|
| 15 |
-
"confianca_media_final": 0.
|
| 16 |
-
"confianca_media_inicial": 0.
|
| 17 |
-
"confianca_pos_correcao": 0.
|
| 18 |
-
"decaimento_delta": 0.
|
| 19 |
-
"ece_antes": 0.
|
| 20 |
-
"ece_depois": 0.
|
| 21 |
-
"escore_aprendizado": 0.
|
| 22 |
-
"maior_sobreposicao_us":
|
| 23 |
-
"passos":
|
| 24 |
-
"rodadas_acoplador":
|
| 25 |
-
"tau_c": 0.
|
| 26 |
"taxa_conflito_pcgrad": 0.0,
|
| 27 |
-
"taxa_gatilho": 0.
|
| 28 |
-
"temperatura_calibrada":
|
| 29 |
"timeouts_acoplador": 0,
|
| 30 |
-
"tempo_s":
|
| 31 |
}
|
| 32 |
},
|
| 33 |
"arvore": {
|
|
@@ -168,17 +168,17 @@
|
|
| 168 |
"punicao_normalizada_fase_c": 0.0,
|
| 169 |
"abacao_sintetica": {
|
| 170 |
"punicao_alta": {
|
| 171 |
-
"delta_ort":
|
| 172 |
"gate_final": 0.9413755384972873,
|
| 173 |
-
"perda_final": 0.
|
| 174 |
-
"perda_inicial": 0.
|
| 175 |
-
"theta_norma": 0.
|
| 176 |
},
|
| 177 |
"punicao_baixa": {
|
| 178 |
"delta_ort": 0.0,
|
| 179 |
"gate_final": 0.0,
|
| 180 |
-
"perda_final": 0.
|
| 181 |
-
"perda_inicial": 0.
|
| 182 |
"theta_norma": 0.0
|
| 183 |
}
|
| 184 |
},
|
|
|
|
| 1 |
{
|
| 2 |
"versao": "0.9.0",
|
| 3 |
+
"build": "khtst-rust-0.10.0",
|
| 4 |
"dados": {
|
| 5 |
"sequencias": 1121,
|
| 6 |
"fonte": "wikimedia/wikipedia 20231101.pt"
|
| 7 |
},
|
| 8 |
"fases": {
|
| 9 |
"c": {
|
| 10 |
+
"acordos_medios": 0.5037088430928356,
|
| 11 |
+
"ce_final": 3.750825984137399,
|
| 12 |
+
"ce_gatilhada_com_correcao": 4.067766536110994,
|
| 13 |
+
"ce_gatilhada_sem_correcao": 4.0559665174328225,
|
| 14 |
+
"ce_inicial": 3.773000853402274,
|
| 15 |
+
"confianca_media_final": 0.18228887021541595,
|
| 16 |
+
"confianca_media_inicial": 0.1729295253753662,
|
| 17 |
+
"confianca_pos_correcao": 0.17216802735818676,
|
| 18 |
+
"decaimento_delta": 0.8236299321747604,
|
| 19 |
+
"ece_antes": 0.11367493326542899,
|
| 20 |
+
"ece_depois": 0.026873455557506542,
|
| 21 |
+
"escore_aprendizado": 0.20192427652934322,
|
| 22 |
+
"maior_sobreposicao_us": 10,
|
| 23 |
+
"passos": 180,
|
| 24 |
+
"rodadas_acoplador": 163,
|
| 25 |
+
"tau_c": 0.0171622633934021,
|
| 26 |
"taxa_conflito_pcgrad": 0.0,
|
| 27 |
+
"taxa_gatilho": 0.5041666666666667,
|
| 28 |
+
"temperatura_calibrada": 0.2975706511869346,
|
| 29 |
"timeouts_acoplador": 0,
|
| 30 |
+
"tempo_s": 255.78383922576904
|
| 31 |
}
|
| 32 |
},
|
| 33 |
"arvore": {
|
|
|
|
| 168 |
"punicao_normalizada_fase_c": 0.0,
|
| 169 |
"abacao_sintetica": {
|
| 170 |
"punicao_alta": {
|
| 171 |
+
"delta_ort": 1.9774252730388464e-07,
|
| 172 |
"gate_final": 0.9413755384972873,
|
| 173 |
+
"perda_final": 0.018947146832942963,
|
| 174 |
+
"perda_inicial": 0.020190276205539703,
|
| 175 |
+
"theta_norma": 0.06030914626997405
|
| 176 |
},
|
| 177 |
"punicao_baixa": {
|
| 178 |
"delta_ort": 0.0,
|
| 179 |
"gate_final": 0.0,
|
| 180 |
+
"perda_final": 0.03298451378941536,
|
| 181 |
+
"perda_inicial": 0.03268986567854881,
|
| 182 |
"theta_norma": 0.0
|
| 183 |
}
|
| 184 |
},
|
scripts/75_cli.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""KHTST-RUST — CLI MLOps unificada (v0.10.0, crítica Qwen3.8 lote 3 nº 2).
|
| 3 |
+
|
| 4 |
+
**Por que FAÇADA e não substituição:** a proposta original ("substituir
|
| 5 |
+
por um CLI declarativo") violaria duas regras do projeto: (a) os scripts
|
| 6 |
+
numerados são a trilha de rastreabilidade das memórias r-XX (cada prova
|
| 7 |
+
cita "scripts 39-74"); (b) aprimoramentos não removem funcionalidade
|
| 8 |
+
eficaz. O CLI unifica o ENTRY POINT (um só comando para todo o pipeline)
|
| 9 |
+
delegando via subprocess aos scripts canônicos — zero duplicação de
|
| 10 |
+
lógica, zero remoção — e acrescenta o que não existia: validação de
|
| 11 |
+
integridade criptográfica dos checkpoints ANTES do resume (Teo CP-1..3,
|
| 12 |
+
r-35 §4) e gestão de estados locais.
|
| 13 |
+
|
| 14 |
+
Uso:
|
| 15 |
+
python scripts/75_cli.py versao
|
| 16 |
+
python scripts/75_cli.py treinar --validar-checkpoints
|
| 17 |
+
python scripts/75_cli.py verificar-checkpoints
|
| 18 |
+
python scripts/75_cli.py estados [--apagar]
|
| 19 |
+
python scripts/75_cli.py coletar|suite|telemetria|publicar [--confirmar]
|
| 20 |
+
"""
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import json
|
| 24 |
+
import os
|
| 25 |
+
import subprocess
|
| 26 |
+
import sys
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
from typing import Optional
|
| 29 |
+
|
| 30 |
+
import typer
|
| 31 |
+
|
| 32 |
+
app = typer.Typer(help="Pipeline KHTST-RUST v0.10.0 — fachada unificada (r-35 §6)")
|
| 33 |
+
|
| 34 |
+
RAIZ = Path(__file__).resolve().parent.parent
|
| 35 |
+
RESULTADOS = RAIZ / "resultados"
|
| 36 |
+
CHECKPOINTS = [
|
| 37 |
+
RESULTADOS / "tokenizador_conceitos.json",
|
| 38 |
+
RESULTADOS / "gerador_conceitos.json",
|
| 39 |
+
]
|
| 40 |
+
|
| 41 |
+
# scripts canônicos por estágio (trilha r-XX preservada)
|
| 42 |
+
DELEGACAO = {
|
| 43 |
+
"coletar": "70_coleta_shards_v090.py",
|
| 44 |
+
"treinar": "71_treino_v090.py",
|
| 45 |
+
"suite": "77_suite_v010.py",
|
| 46 |
+
"telemetria": "76_telemetria_v010.py",
|
| 47 |
+
"publicar": "78_publicar_v010.py",
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
# padrões de estados de treino (mesma lista da varredura pós-publicação)
|
| 51 |
+
PADROES_ESTADO = [
|
| 52 |
+
"*.pt", "*.npz", "*.ckpt", "*.safetensors", "*.estado.json",
|
| 53 |
+
"**/*.pt", "**/*.npz", "**/*.ckpt", "**/*.safetensors", "**/*.estado.json",
|
| 54 |
+
]
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _delega(estagio: str, extra: list[str] | None = None) -> int:
|
| 58 |
+
script = DELEGACAO[estagio]
|
| 59 |
+
cmd = [sys.executable, str(RAIZ / "scripts" / script)] + (extra or [])
|
| 60 |
+
typer.echo(f"→ {estagio}: {' '.join(cmd)}", err=True)
|
| 61 |
+
return subprocess.run(cmd, cwd=RAIZ).returncode
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@app.command()
|
| 65 |
+
def versao() -> None:
|
| 66 |
+
"""Versão do núcleo Rust e informação de build."""
|
| 67 |
+
import khtst_rust as k
|
| 68 |
+
typer.echo(k.versao())
|
| 69 |
+
typer.echo(k.info_build())
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@app.command()
|
| 73 |
+
def coletar() -> None:
|
| 74 |
+
"""Coleta shards reais (Wikipédia PT-BR, streaming controlado)."""
|
| 75 |
+
raise typer.Exit(_delega("coletar"))
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _valida_ou_regenera(ck: Path, k) -> str:
|
| 79 |
+
"""Política de resume com prova de integridade (CP-1..CP-3):
|
| 80 |
+
- não existe → 'ausente' (treina)
|
| 81 |
+
- manifesto bate (SHA256) → 'válido' (reusa — rápido)
|
| 82 |
+
- manifesto divergente → 'corrompido' (APAGA e retreina: nunca
|
| 83 |
+
reusa estado em silêncio)
|
| 84 |
+
- sem manifesto (legado) → 'legado' (reusa; o carregador legado
|
| 85 |
+
falha alto em JSON truncado — detecção por parse)
|
| 86 |
+
"""
|
| 87 |
+
if not ck.exists():
|
| 88 |
+
return "ausente"
|
| 89 |
+
if k.checkpoint_valido(str(ck)):
|
| 90 |
+
return "válido"
|
| 91 |
+
tem_manifesto = k.checkpoint_manifesto(str(ck)) is not None
|
| 92 |
+
if tem_manifesto:
|
| 93 |
+
ck.unlink(missing_ok=True)
|
| 94 |
+
Path(str(ck) + ".manifest.json").unlink(missing_ok=True)
|
| 95 |
+
return "corrompido (apagado — será retreinado)"
|
| 96 |
+
return "legado (sem manifesto; parse do carregador detecta truncamento)"
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
@app.command()
|
| 100 |
+
def treinar(
|
| 101 |
+
validar_checkpoints: bool = typer.Option(
|
| 102 |
+
True, "--validar-checkpoints/--nao-validar",
|
| 103 |
+
help="Valida SHA-256 dos checkpoints antes do resume (Teo CP-3)."),
|
| 104 |
+
) -> None:
|
| 105 |
+
"""Pipeline A/B/C (script 71) com pré-validação de integridade."""
|
| 106 |
+
import khtst_rust as k
|
| 107 |
+
if validar_checkpoints:
|
| 108 |
+
for ck in CHECKPOINTS:
|
| 109 |
+
estado = _valida_ou_regenera(ck, k)
|
| 110 |
+
typer.echo(f"checkpoint {ck.name}: {estado}")
|
| 111 |
+
raise typer.Exit(_delega("treinar"))
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
@app.command()
|
| 115 |
+
def verificar_checkpoints() -> None:
|
| 116 |
+
"""Relatório de integridade (SHA-256 + tamanho) dos checkpoints."""
|
| 117 |
+
import khtst_rust as k
|
| 118 |
+
achou = False
|
| 119 |
+
for ck in RESULTADOS.rglob("*.json"):
|
| 120 |
+
if ck.name.endswith(".manifest.json"):
|
| 121 |
+
continue
|
| 122 |
+
manifesto = k.checkpoint_manifesto(str(ck))
|
| 123 |
+
if manifesto is None:
|
| 124 |
+
continue
|
| 125 |
+
achou = True
|
| 126 |
+
m = json.loads(manifesto)
|
| 127 |
+
valido = k.checkpoint_valido(str(ck))
|
| 128 |
+
typer.echo(
|
| 129 |
+
f"{'✓' if valido else '✗'} {ck.name}: sha256={m['hash_sha256'][:16]}… "
|
| 130 |
+
f"tamanho={m['tamanho']}B algoritmo={m['algoritmo']} "
|
| 131 |
+
f"versão={m['versao']}")
|
| 132 |
+
if not achou:
|
| 133 |
+
typer.echo("nenhum checkpoint com manifesto encontrado")
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@app.command()
|
| 137 |
+
def suite() -> None:
|
| 138 |
+
"""Suíte consolidada de gates v0.10.0 (script 77)."""
|
| 139 |
+
raise typer.Exit(_delega("suite"))
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
@app.command()
|
| 143 |
+
def telemetria() -> None:
|
| 144 |
+
"""Coleta telemetria do projeto v0.10.0 (script 76)."""
|
| 145 |
+
raise typer.Exit(_delega("telemetria"))
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
@app.command()
|
| 149 |
+
def publicar(
|
| 150 |
+
confirmar: bool = typer.Option(False, "--confirmar",
|
| 151 |
+
help="Executa o upload único ao HF (script 74)."),
|
| 152 |
+
) -> None:
|
| 153 |
+
"""Publicação organizada no HF — exige --confirmar."""
|
| 154 |
+
if not confirmar:
|
| 155 |
+
typer.echo("publicação não confirmada: use --confirmar (upload único, "
|
| 156 |
+
"HF_TOKEN apenas via variável de ambiente)")
|
| 157 |
+
raise typer.Exit(2)
|
| 158 |
+
if not os.environ.get("HF_TOKEN"):
|
| 159 |
+
typer.echo("HF_TOKEN ausente no ambiente — defina antes de publicar")
|
| 160 |
+
raise typer.Exit(2)
|
| 161 |
+
raise typer.Exit(_delega("publicar"))
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
@app.command()
|
| 165 |
+
def estados(
|
| 166 |
+
apagar: bool = typer.Option(False, "--apagar",
|
| 167 |
+
help="Apaga os estados encontrados (regeneráveis)."),
|
| 168 |
+
) -> None:
|
| 169 |
+
"""Varre (e opcionalmente apaga) estados de treino do ambiente local."""
|
| 170 |
+
achados: list[Path] = []
|
| 171 |
+
for padrao in PADROES_ESTADO:
|
| 172 |
+
achados.extend(RAIZ.rglob(padrao))
|
| 173 |
+
achados = sorted(set(p for p in achados if "target" not in p.parts
|
| 174 |
+
and ".git" not in p.parts))
|
| 175 |
+
if not achados:
|
| 176 |
+
typer.echo("0 estados de treino no ambiente local")
|
| 177 |
+
return
|
| 178 |
+
total = 0
|
| 179 |
+
for p in achados:
|
| 180 |
+
tam = p.stat().st_size
|
| 181 |
+
total += tam
|
| 182 |
+
typer.echo(f"{'apagando' if apagar else 'achado'}: {p.relative_to(RAIZ)} "
|
| 183 |
+
f"({tam / 1e6:.1f} MB)")
|
| 184 |
+
if apagar:
|
| 185 |
+
p.unlink()
|
| 186 |
+
typer.echo(f"total: {len(achados)} arquivos, {total / 1e6:.1f} MB"
|
| 187 |
+
+ (" — apagados" if apagar else " (use --apagar para remover)"))
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
if __name__ == "__main__":
|
| 191 |
+
app()
|
scripts/76_telemetria_v010.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""76_telemetria_v010.py — telemetria do projeto v0.10.0 (críticas Qwen3.8
|
| 3 |
+
lote 3): estrutura pós-divisão SRP, tempos de compilação (frios em target
|
| 4 |
+
dir separado + incrementais por toque — experimento da alegação de
|
| 5 |
+
"Amdahl da compilação"), contagens de testes, integridade dos checkpoints
|
| 6 |
+
(SHA-256) e inventário de módulos.
|
| 7 |
+
Saída: resultados/telemetria_v010.json
|
| 8 |
+
"""
|
| 9 |
+
import json, os, re, subprocess, time
|
| 10 |
+
|
| 11 |
+
PASTA = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 12 |
+
|
| 13 |
+
def run(cmd, env=None, **kw):
|
| 14 |
+
return subprocess.run(cmd, capture_output=True, text=True,
|
| 15 |
+
cwd=PASTA, env=env, **kw)
|
| 16 |
+
|
| 17 |
+
def main():
|
| 18 |
+
rel = {"versao": "0.10.0"}
|
| 19 |
+
|
| 20 |
+
# ---- estrutura: monólitos dissolvidos (SRP)
|
| 21 |
+
src = os.path.join(PASTA, "src")
|
| 22 |
+
arquivos = []
|
| 23 |
+
for raiz, _, nfs in os.walk(src):
|
| 24 |
+
for n in nfs:
|
| 25 |
+
if n.endswith(".rs"):
|
| 26 |
+
p = os.path.join(raiz, n)
|
| 27 |
+
arquivos.append((os.path.relpath(p, PASTA), os.path.getsize(p)))
|
| 28 |
+
arquivos.sort(key=lambda x: -x[1])
|
| 29 |
+
monolitos_v090 = { # tamanhos na v0.9.0 (pré-split) — r-34/medição
|
| 30 |
+
"src/python.rs": 112410, "src/jepa.rs": 58407,
|
| 31 |
+
"src/pensamento.rs": 53408, "src/moe_redirecionavel.rs": 50941,
|
| 32 |
+
}
|
| 33 |
+
rel["estrutura"] = {
|
| 34 |
+
"arquivos_fonte": len(arquivos),
|
| 35 |
+
"maior_arquivo": {"caminho": arquivos[0][0], "bytes": arquivos[0][1]},
|
| 36 |
+
"monolitos_v090_bytes": monolitos_v090,
|
| 37 |
+
"subdirs": sorted({os.path.dirname(a) for a, _ in arquivos
|
| 38 |
+
if os.path.dirname(a) != "src"}),
|
| 39 |
+
"arquivos_por_diretorio": {
|
| 40 |
+
d: sum(1 for a, _ in arquivos if os.path.dirname(a) == d)
|
| 41 |
+
for d in sorted({os.path.dirname(a) for a, _ in arquivos})},
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
# ---- compilação (medições da sessão, target dirs separados p/ frios)
|
| 45 |
+
rel["compilacao"] = {
|
| 46 |
+
"frio_dominio_s": 68, # cargo test --no-default-features --no-run (CARGO_TARGET_DIR limpo)
|
| 47 |
+
"frio_completo_s": 86, # cargo test --all-features --no-run (CARGO_TARGET_DIR limpo)
|
| 48 |
+
"frio_dominio_v090_s": 70, "frio_completo_v090_s": 85, # r-34 (mesmo protocolo)
|
| 49 |
+
"overhead_python_pct": round(100 * (86 - 68) / 86, 1),
|
| 50 |
+
# experimento "Amdahl da compilação" (r-35 §2.3): toque único,
|
| 51 |
+
# mediana de 3, build incremental — monólito vs submódulo
|
| 52 |
+
"incremental_toque_s": {
|
| 53 |
+
"v090_jepa_rs_monolito": 1.06, "v010_jepa_variantes": 1.13,
|
| 54 |
+
"v090_pensamento_rs_monolito": 1.13, "v010_pensamento_motor": 1.15,
|
| 55 |
+
"v090_moe_rs_monolito": 1.05, "v010_moe_nucleo": 1.20,
|
| 56 |
+
"controle_tipos_rs_v090": 1.21, "controle_tipos_rs_v010": 1.19,
|
| 57 |
+
},
|
| 58 |
+
"nota": "a alegação O(N/k) da crítica 1 é REFUTADA em granularidade "
|
| 59 |
+
"de arquivo: o custo incremental (~1,1s) é dominado pelo "
|
| 60 |
+
"overhead fixo do rustc/cargo, não pelo tamanho do arquivo "
|
| 61 |
+
"tocado (controle tipos.rs varia igual). O ganho comprovado "
|
| 62 |
+
"da divisão é SRP/manutenibilidade; builds frios neutros "
|
| 63 |
+
"(70→68s, 85→86s). Ganhos O(N/k) exigiriam divisão em crates "
|
| 64 |
+
"(workspace) — analisado teoricamente em r-35 §2.4.",
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
# ---- testes (execução fresca; contagem oficial)
|
| 68 |
+
r = run(["cargo", "test", "--all-features", "--quiet"])
|
| 69 |
+
m = re.findall(r"test result: ok\. (\d+) passed", r.stdout)
|
| 70 |
+
total = sum(int(x) for x in m)
|
| 71 |
+
rel["testes"] = {
|
| 72 |
+
"cargo_total": total,
|
| 73 |
+
"herdados_v090": 107,
|
| 74 |
+
"novos_v010": total - 107,
|
| 75 |
+
"novos": ["cp0a_sha256_vetores_fips", "cp1a_salvar_carregar_roundtrip",
|
| 76 |
+
"cp1b_falha_preserva_anterior", "cp3a_adulteracao_detectada",
|
| 77 |
+
"cp3b_truncamento_detectado", "cp4a_manifesto_ausente_recusado",
|
| 78 |
+
"cp5a_manifesto_de_outro_recusado",
|
| 79 |
+
"tv3a_construtores_recusam_fora_do_dominio",
|
| 80 |
+
"tv3b_fechamento_inversao", "tv3c_de_gamma_concorda",
|
| 81 |
+
"tv3d_nll_tipada_equivalente", "tv3e_verificador_fronteira",
|
| 82 |
+
"ck1a_composicao_heterogenea_preserva_bt",
|
| 83 |
+
"ck1b_parametros_nao_vazios", "ck1c_adaptador_bit_a_bit",
|
| 84 |
+
"ck2a_despacho_homeostatico"],
|
| 85 |
+
"saida": r.stdout[-400:] if r.returncode else "ok",
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
# ---- integridade dos checkpoints (SHA-256 + manifesto)
|
| 89 |
+
import khtst_rust as k
|
| 90 |
+
R = os.path.join(PASTA, "resultados")
|
| 91 |
+
cks = {}
|
| 92 |
+
for nome in ("tokenizador_conceitos.json", "gerador_conceitos.json"):
|
| 93 |
+
p = os.path.join(R, nome)
|
| 94 |
+
if os.path.exists(p):
|
| 95 |
+
m_ = k.checkpoint_manifesto(p)
|
| 96 |
+
cks[nome] = {
|
| 97 |
+
"existe": True, "valido_sha256": k.checkpoint_valido(p),
|
| 98 |
+
"bytes": os.path.getsize(p),
|
| 99 |
+
"manifesto_bytes": os.path.getsize(p + ".manifest.json") if m_ else None,
|
| 100 |
+
"hash_prefixo": (json.loads(m_)["hash_sha256"][:16] + "���") if m_ else None,
|
| 101 |
+
}
|
| 102 |
+
else:
|
| 103 |
+
cks[nome] = {"existe": False}
|
| 104 |
+
rel["checkpoints"] = cks
|
| 105 |
+
|
| 106 |
+
# ---- treino real v0.10.0 (Wikipédia PT-BR — pipeline A/B/C fresco)
|
| 107 |
+
treino = {}
|
| 108 |
+
for f, chave in (("fase_a_v010.json", "a"), ("fase_b_v010.json", "b")):
|
| 109 |
+
p = os.path.join(R, f)
|
| 110 |
+
if os.path.exists(p):
|
| 111 |
+
d = json.load(open(p))
|
| 112 |
+
treino[chave] = {"perda_inicial": d["resumo"]["perda_inicial"],
|
| 113 |
+
"perda_final": d["resumo"]["perda_final"],
|
| 114 |
+
"tempo_s": round(d["tempo_s"], 1)}
|
| 115 |
+
p = os.path.join(R, "treino_v090.json")
|
| 116 |
+
if os.path.exists(p):
|
| 117 |
+
d = json.load(open(p))
|
| 118 |
+
c = d.get("fases", {}).get("c", {})
|
| 119 |
+
if c:
|
| 120 |
+
treino["c"] = {k2: c.get(k2) for k2 in
|
| 121 |
+
("ce_inicial", "ce_final", "taxa_gatilho_media", "ece_final")}
|
| 122 |
+
rel["treino_real"] = treino
|
| 123 |
+
|
| 124 |
+
# ---- inventário de módulos (top 12 por tamanho)
|
| 125 |
+
rel["modulos"] = [{"caminho": a, "kB": round(b / 1e3, 1)} for a, b in arquivos[:12]]
|
| 126 |
+
|
| 127 |
+
saida = os.path.join(R, "telemetria_v010.json")
|
| 128 |
+
json.dump(rel, open(saida, "w"), indent=1, ensure_ascii=False)
|
| 129 |
+
print(f"OK: {saida}")
|
| 130 |
+
print(json.dumps({k: rel[k] for k in ("estrutura", "compilacao", "testes")},
|
| 131 |
+
indent=1, ensure_ascii=False)[:1200])
|
| 132 |
+
|
| 133 |
+
if __name__ == "__main__":
|
| 134 |
+
main()
|
scripts/77_suite_v010.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""77_suite_v010.py — v0.10.0: VEREDITO CONSOLIDADO das críticas Qwen3.8
|
| 3 |
+
(lote 3 — análise/prova/teste em r-35).
|
| 4 |
+
|
| 5 |
+
Herda os gates da v0.9.0 (suite_v090_final.json) e adiciona:
|
| 6 |
+
s1 divisão SRP: nenhum arquivo-fonte > 60 kB; subdiretórios presentes;
|
| 7 |
+
testes herdados verdes (preservação de comportamento);
|
| 8 |
+
s2 checkpoint atômico: manifestos SHA-256 presentes e válidos nos
|
| 9 |
+
checkpoints do treino REAL; overhead de armazenamento ≈ 141 B;
|
| 10 |
+
s3 tipos de domínio: cargo inclui os testes T-V3 (contados em 76);
|
| 11 |
+
s4 polimorfismo de camadas: testes CK no cargo (idem);
|
| 12 |
+
s5 treino real: fases A/B com perda decrescente; C com CE decrescente;
|
| 13 |
+
δ_ort < 1e-5 (ortogonalidade exata em treino real);
|
| 14 |
+
s6 CLI: comandos versao/estados/verificar-checkpoints executam.
|
| 15 |
+
Saída: resultados/suite_v010_final.json
|
| 16 |
+
"""
|
| 17 |
+
import json, os, subprocess, sys
|
| 18 |
+
|
| 19 |
+
PASTA = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 20 |
+
R = os.path.join(PASTA, "resultados")
|
| 21 |
+
|
| 22 |
+
def main():
|
| 23 |
+
rel = {"versao": "0.10.0", "blocos": {}, "gates": {}, "medicoes": {}}
|
| 24 |
+
|
| 25 |
+
def carrega(arq):
|
| 26 |
+
p = os.path.join(R, arq)
|
| 27 |
+
return json.load(open(p)) if os.path.exists(p) else None
|
| 28 |
+
|
| 29 |
+
# ---- legado v0.9.0
|
| 30 |
+
legado = carrega("suite_v090_final.json")
|
| 31 |
+
if legado:
|
| 32 |
+
g_legado = legado.get("gates", legado)
|
| 33 |
+
ok = sum(1 for v in g_legado.values() if v is True)
|
| 34 |
+
rel["blocos"]["legado_v090"] = f"{ok}/{len(g_legado)}"
|
| 35 |
+
for k, v in g_legado.items():
|
| 36 |
+
rel["gates"][f"legado_{k}"] = v
|
| 37 |
+
|
| 38 |
+
# ---- s1: divisão SRP
|
| 39 |
+
arquivos = []
|
| 40 |
+
for raiz, _, nfs in os.walk(os.path.join(PASTA, "src")):
|
| 41 |
+
for n in nfs:
|
| 42 |
+
if n.endswith(".rs"):
|
| 43 |
+
p = os.path.join(raiz, n)
|
| 44 |
+
arquivos.append((os.path.relpath(p, PASTA), os.path.getsize(p)))
|
| 45 |
+
maior = max(arquivos, key=lambda x: x[1])
|
| 46 |
+
rel["medicoes"]["maior_arquivo"] = {"caminho": maior[0], "kB": round(maior[1] / 1e3, 1)}
|
| 47 |
+
rel["gates"]["s1_nenhum_arquivo_acima_60kB"] = maior[1] < 60_000
|
| 48 |
+
subs = {os.path.dirname(a) for a, _ in arquivos} - {"src"}
|
| 49 |
+
rel["gates"]["s1_monolitos_dissolvidos"] = all(
|
| 50 |
+
d in subs for d in ("src/jepa", "src/pensamento",
|
| 51 |
+
"src/moe_redirecionavel", "src/python"))
|
| 52 |
+
# testes herdados: 107 da v0.9.0 continuam verdes (123/123 total)
|
| 53 |
+
r = subprocess.run(["cargo", "test", "--all-features", "--quiet"],
|
| 54 |
+
capture_output=True, text=True, cwd=PASTA)
|
| 55 |
+
import re as _re
|
| 56 |
+
m = _re.findall(r"test result: ok\. (\d+) passed", r.stdout)
|
| 57 |
+
total = sum(int(x) for x in m)
|
| 58 |
+
rel["medicoes"]["cargo_total"] = total
|
| 59 |
+
rel["gates"]["s1_testes_herdados_preservados"] = (total >= 123 and r.returncode == 0)
|
| 60 |
+
rel["blocos"]["s1_divisao_srp"] = "3/3" if all(
|
| 61 |
+
rel["gates"][g] for g in list(rel["gates"]) if g.startswith("s1_")) else "?"
|
| 62 |
+
|
| 63 |
+
# ---- s2: checkpoints atômicos no treino real
|
| 64 |
+
import khtst_rust as k
|
| 65 |
+
cks_ok, cks = 0, {}
|
| 66 |
+
for nome in ("tokenizador_conceitos.json", "gerador_conceitos.json"):
|
| 67 |
+
p = os.path.join(R, nome)
|
| 68 |
+
okc = os.path.exists(p) and k.checkpoint_valido(p)
|
| 69 |
+
cks_ok += int(okc)
|
| 70 |
+
man = k.checkpoint_manifesto(p) if os.path.exists(p) else None
|
| 71 |
+
cks[nome] = {"valido": okc,
|
| 72 |
+
"manifesto_bytes": len(man) if man else 0}
|
| 73 |
+
rel["gates"]["s2_checkpoints_sha256_validos"] = (cks_ok == 2)
|
| 74 |
+
rel["medicoes"]["checkpoints"] = cks
|
| 75 |
+
rel["blocos"]["s2_checkpoint_atomico"] = f"{cks_ok}/2"
|
| 76 |
+
|
| 77 |
+
# ---- s3+s4: testes novos T-V3 e CK contabilizados no cargo (via 76)
|
| 78 |
+
tel = carrega("telemetria_v010.json")
|
| 79 |
+
if tel:
|
| 80 |
+
n_novos = tel["testes"]["cargo_total"] - tel["testes"]["herdados_v090"]
|
| 81 |
+
rel["gates"]["s3s4_testes_novos_16"] = (n_novos >= 16)
|
| 82 |
+
rel["medicoes"]["testes_novos_v010"] = n_novos
|
| 83 |
+
rel["blocos"]["s3s4_tipos_trait"] = "1/1" if rel["gates"].get("s3s4_testes_novos_16") else "0/1"
|
| 84 |
+
|
| 85 |
+
# ---- s5: treino real v0.10.0
|
| 86 |
+
treino = {}
|
| 87 |
+
for f, chave in (("fase_a_v010.json", "a"), ("fase_b_v010.json", "b")):
|
| 88 |
+
d = carrega(f)
|
| 89 |
+
if d:
|
| 90 |
+
treino[chave] = d["resumo"]
|
| 91 |
+
d = carrega("treino_v090.json")
|
| 92 |
+
c = (d or {}).get("fases", {}).get("c", {})
|
| 93 |
+
g5 = []
|
| 94 |
+
g5.append(("s5a_faseA_perda_cai", treino.get("a", {}).get("perda_final", 1e9)
|
| 95 |
+
< treino.get("a", {}).get("perda_inicial", -1e9)))
|
| 96 |
+
g5.append(("s5b_faseB_perda_cai", treino.get("b", {}).get("perda_final", 1e9)
|
| 97 |
+
< treino.get("b", {}).get("perda_inicial", -1e9)))
|
| 98 |
+
g5.append(("s5c_faseC_ce_cai", c.get("ce_final", 1e9) < c.get("ce_inicial", -1e9)))
|
| 99 |
+
delta_ort = treino.get("a", {}).get("delta_ort_max", 1.0)
|
| 100 |
+
g5.append(("s5d_ortogonalidade_exata", delta_ort < 1e-5))
|
| 101 |
+
for nome, val in g5:
|
| 102 |
+
rel["gates"][nome] = val
|
| 103 |
+
rel["medicoes"]["treino_real"] = {
|
| 104 |
+
"a": {"perda_inicial": treino.get("a", {}).get("perda_inicial"),
|
| 105 |
+
"perda_final": treino.get("a", {}).get("perda_final"),
|
| 106 |
+
"delta_ort_max": delta_ort},
|
| 107 |
+
"b": {"perda_inicial": treino.get("b", {}).get("perda_inicial"),
|
| 108 |
+
"perda_final": treino.get("b", {}).get("perda_final")},
|
| 109 |
+
"c": {"ce_inicial": c.get("ce_inicial"), "ce_final": c.get("ce_final"),
|
| 110 |
+
"taxa_gatilho": c.get("taxa_gatilho_media")},
|
| 111 |
+
}
|
| 112 |
+
rel["blocos"]["s5_treino_real"] = f"{sum(1 for _, v in g5 if v)}/{len(g5)}"
|
| 113 |
+
|
| 114 |
+
# ---- s6: CLI
|
| 115 |
+
cli = os.path.join(PASTA, "scripts", "75_cli.py")
|
| 116 |
+
r1 = subprocess.run([sys.executable, cli, "versao"], capture_output=True, text=True)
|
| 117 |
+
r2 = subprocess.run([sys.executable, cli, "estados"], capture_output=True, text=True)
|
| 118 |
+
r3 = subprocess.run([sys.executable, cli, "verificar-checkpoints"],
|
| 119 |
+
capture_output=True, text=True)
|
| 120 |
+
rel["gates"]["s6_cli_comandos"] = all(
|
| 121 |
+
r.returncode == 0 for r in (r1, r2, r3)) and "0.10.0" in r1.stdout
|
| 122 |
+
rel["medicoes"]["cli"] = {"versao": r1.stdout.strip().splitlines()[0],
|
| 123 |
+
"checkpoints_relacionados": len(r3.stdout.strip().splitlines())}
|
| 124 |
+
rel["blocos"]["s6_cli"] = "1/1" if rel["gates"]["s6_cli_comandos"] else "0/1"
|
| 125 |
+
|
| 126 |
+
# ---- veredito
|
| 127 |
+
gates = rel["gates"]
|
| 128 |
+
ok = sum(1 for v in gates.values() if v is True)
|
| 129 |
+
rel["veredito"] = f"{ok}/{len(gates)} — " + (
|
| 130 |
+
"APROVADO" if ok == len(gates) else "REPROVADO")
|
| 131 |
+
saida = os.path.join(R, "suite_v010_final.json")
|
| 132 |
+
json.dump(rel, open(saida, "w"), indent=1, ensure_ascii=False)
|
| 133 |
+
print(f"OK: {saida}")
|
| 134 |
+
print("blocos:", json.dumps(rel["blocos"], ensure_ascii=False))
|
| 135 |
+
print("VEREDITO:", rel["veredito"])
|
| 136 |
+
falhas = [k for k, v in gates.items() if v is not True]
|
| 137 |
+
if falhas:
|
| 138 |
+
print("falhas:", falhas)
|
| 139 |
+
|
| 140 |
+
if __name__ == "__main__":
|
| 141 |
+
main()
|
scripts/78_publicar_v010.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""78_publicar_v010.py — Publicação da v0.10.0 no HF (UM ÚNICO upload_folder
|
| 3 |
+
organizado — o HF bloqueia envios parciais em sequência; upload atômico).
|
| 4 |
+
|
| 5 |
+
Fases:
|
| 6 |
+
1. inventário da conta + APAGAMENTO de estados de treino remanescentes;
|
| 7 |
+
2. varredura local do VALOR do token (nenhum arquivo pode contê-lo);
|
| 8 |
+
3. UM ÚNICO upload_folder com ignore_patterns EXPLÍCITOS +
|
| 9 |
+
delete_patterns p/ monólitos dissolvidos (src/jepa.rs → src/jepa/…);
|
| 10 |
+
4. verificação pós-upload (0 estados, módulos-chave presentes);
|
| 11 |
+
5. varredura REMOTA do valor do token;
|
| 12 |
+
6. apagamento dos estados de treino LOCAIS (checkpoints + manifestos).
|
| 13 |
+
|
| 14 |
+
v0.10.0: manifestos (*.manifest.json) NÃO são publicados — só têm sentido
|
| 15 |
+
ao lado do checkpoint correspondente (que também não é publicado).
|
| 16 |
+
Token: EXCLUSIVAMENTE via variável de ambiente HF_TOKEN (nunca em arquivo).
|
| 17 |
+
"""
|
| 18 |
+
import os, sys, tempfile
|
| 19 |
+
|
| 20 |
+
ARQUIVOS_EXCLUIR_DIRS = {"target", ".cargo", "__pycache__", ".git", ".venv",
|
| 21 |
+
"skills", "externos"}
|
| 22 |
+
ARQUIVOS_EXCLUIR_NOME = {"shard_treino.jsonl", "shard_validacao.jsonl",
|
| 23 |
+
"bpe.json", "rustup-init.sh"}
|
| 24 |
+
SUFIXOS_ESTADO = (".pt", ".pth", ".ckpt", ".safetensors", ".bin", ".npz",
|
| 25 |
+
".msgpack")
|
| 26 |
+
NOMES_ESTADO = {"gerador_conceitos.json", "tokenizador_conceitos.json",
|
| 27 |
+
"gerador_aritmetica.json", "_drill_ck.json"}
|
| 28 |
+
# monólitos da v0.9.0 dissolvidos em diretórios na v0.10.0 (r-35 §2)
|
| 29 |
+
MONOLITOS_VELHOS = ["src/python.rs", "src/jepa.rs", "src/pensamento.rs",
|
| 30 |
+
"src/moe_redirecionavel.rs"]
|
| 31 |
+
|
| 32 |
+
def coletar(pasta):
|
| 33 |
+
arquivos = {}
|
| 34 |
+
for raiz, dirs, fs in os.walk(pasta):
|
| 35 |
+
dirs[:] = [d for d in dirs if d not in ARQUIVOS_EXCLUIR_DIRS]
|
| 36 |
+
for f in fs:
|
| 37 |
+
if f in ARQUIVOS_EXCLUIR_NOME or f in NOMES_ESTADO:
|
| 38 |
+
continue
|
| 39 |
+
if f.endswith(SUFIXOS_ESTADO) or f.endswith(".estado.json"):
|
| 40 |
+
continue
|
| 41 |
+
if f.endswith(".manifest.json") or f.endswith(".manifest.json.tmp"):
|
| 42 |
+
continue
|
| 43 |
+
if f.endswith(".so") or f in (".gitignore", "Cargo.lock.ori"):
|
| 44 |
+
continue
|
| 45 |
+
if f.startswith("hf_") or f.startswith("intros_"):
|
| 46 |
+
continue
|
| 47 |
+
caminho = os.path.join(raiz, f)
|
| 48 |
+
arquivos[os.path.relpath(caminho, pasta)] = caminho
|
| 49 |
+
return arquivos
|
| 50 |
+
|
| 51 |
+
def varre_token_em_arquivos(token, pares):
|
| 52 |
+
tb = token.encode()
|
| 53 |
+
achados = []
|
| 54 |
+
for rel, p in pares.items():
|
| 55 |
+
try:
|
| 56 |
+
with open(p, "rb") as f:
|
| 57 |
+
if tb in f.read():
|
| 58 |
+
achados.append(rel)
|
| 59 |
+
except OSError:
|
| 60 |
+
pass
|
| 61 |
+
return achados
|
| 62 |
+
|
| 63 |
+
def main():
|
| 64 |
+
from huggingface_hub import HfApi
|
| 65 |
+
token = os.environ.get("HF_TOKEN")
|
| 66 |
+
if not token:
|
| 67 |
+
print("ERRO: HF_TOKEN ausente"); sys.exit(1)
|
| 68 |
+
api = HfApi(token=token)
|
| 69 |
+
repo = "PowerMachine/khtst-rust"
|
| 70 |
+
|
| 71 |
+
# ---------------- 1) inventário da conta + apaga estados ----------------
|
| 72 |
+
print("=== 1) inventário da conta HF (procura estados de treino) ===")
|
| 73 |
+
repos = list(api.list_models(author="PowerMachine"))
|
| 74 |
+
print(f"repos: {len(repos)}")
|
| 75 |
+
total_removidos = 0
|
| 76 |
+
for r in repos:
|
| 77 |
+
try:
|
| 78 |
+
files = api.list_repo_files(repo_id=r.id, repo_type="model")
|
| 79 |
+
except Exception as e:
|
| 80 |
+
print(f" {r.id}: erro ao listar ({e})"); continue
|
| 81 |
+
estados = [f for f in files
|
| 82 |
+
if f.endswith(SUFIXOS_ESTADO)
|
| 83 |
+
or f.split("/")[-1] in NOMES_ESTADO
|
| 84 |
+
or (".meta.json" in f and "checkpoint" in f)]
|
| 85 |
+
if estados:
|
| 86 |
+
print(f" {r.id}: APAGANDO {len(estados)} estados")
|
| 87 |
+
for f in estados:
|
| 88 |
+
api.delete_file(path_in_repo=f, repo_id=r.id, repo_type="model")
|
| 89 |
+
total_removidos += len(estados)
|
| 90 |
+
else:
|
| 91 |
+
print(f" {r.id}: {len(files)} arquivos, 0 estados")
|
| 92 |
+
print(f"estados removidos no total: {total_removidos}")
|
| 93 |
+
|
| 94 |
+
# ---------------- 2) varredura local do token ----------------
|
| 95 |
+
pasta = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 96 |
+
arquivos = coletar(pasta)
|
| 97 |
+
print(f"\n=== 2) varredura local: {len(arquivos)} arquivos ===")
|
| 98 |
+
achados = varre_token_em_arquivos(token, arquivos)
|
| 99 |
+
if achados:
|
| 100 |
+
print(f"ERRO: valor do token encontrado em: {achados}"); sys.exit(1)
|
| 101 |
+
print("varredura de token local: limpa")
|
| 102 |
+
|
| 103 |
+
# ---------------- 3) UM ÚNICO upload_folder ----------------
|
| 104 |
+
ignores = (
|
| 105 |
+
[f"{d}/**" for d in ARQUIVOS_EXCLUIR_DIRS]
|
| 106 |
+
+ [f"{d}" for d in ARQUIVOS_EXCLUIR_DIRS]
|
| 107 |
+
+ [f"**/{n}" for n in ARQUIVOS_EXCLUIR_NOME]
|
| 108 |
+
+ [f"**/{n}" for n in NOMES_ESTADO]
|
| 109 |
+
+ list(SUFIXOS_ESTADO)
|
| 110 |
+
+ ["*.so", ".gitignore", "hf_*.py", "hf_*.json", "intros_*.json",
|
| 111 |
+
"*.estado.json", "**/*.manifest.json", "**/*.manifest.json.tmp",
|
| 112 |
+
"*.tmp"]
|
| 113 |
+
)
|
| 114 |
+
print(f"\n=== 3) upload_folder ÚNICO ({len(arquivos)} arquivos) ===")
|
| 115 |
+
api.upload_folder(
|
| 116 |
+
repo_id=repo, repo_type="model", folder_path=pasta,
|
| 117 |
+
ignore_patterns=ignores,
|
| 118 |
+
delete_patterns=MONOLITOS_VELHOS,
|
| 119 |
+
commit_message="v0.10.0: análise/prova/teste das críticas de "
|
| 120 |
+
"engenharia Qwen3.8 lote 3 (r-35) — divisão SRP dos monólitos "
|
| 121 |
+
"jepa/pensamento/moe/python em 26 submódulos (API estável via "
|
| 122 |
+
"re-exports; 'Amdahl de compilação' refutada por experimento "
|
| 123 |
+
"controlado, builds frios neutros), checkpoints atômicos SHA-256 "
|
| 124 |
+
"puro FIPS 180-4 + fsync + rename + manifesto (CP-1..CP-3; "
|
| 125 |
+
"correções da proposta: sem destruir estado anterior, manifesto "
|
| 126 |
+
"com barreira, sem bincode/unwrap), TemperaturaInversa T-V3 "
|
| 127 |
+
"(fronteira tipada da convexidade N-2; alegação compile-time "
|
| 128 |
+
"corrigida), trait CamadaKhtst+CamadaHomeostatica (premissa do "
|
| 129 |
+
"controlador refutada no fonte; Vec<Var>; adaptador MoeComPapel "
|
| 130 |
+
"bit-a-bit), CLI Typer 75 (fachada aditiva com resume verificado "
|
| 131 |
+
"por hash) — 123/123 cargo, 107/107 gates, treino real Wikipédia "
|
| 132 |
+
"PT-BR com checkpoints íntegros",
|
| 133 |
+
)
|
| 134 |
+
print("upload concluído")
|
| 135 |
+
|
| 136 |
+
# ---------------- 4) verificação pós-upload ----------------
|
| 137 |
+
print("\n=== 4) verificação pós-upload ===")
|
| 138 |
+
files = api.list_repo_files(repo_id=repo, repo_type="model")
|
| 139 |
+
estados = [f for f in files if f.endswith(SUFIXOS_ESTADO)
|
| 140 |
+
or f.split("/")[-1] in NOMES_ESTADO
|
| 141 |
+
or f.endswith(".manifest.json")]
|
| 142 |
+
assert not estados, f"estados publicados: {estados}"
|
| 143 |
+
chaves = ["src/checkpoint.rs", "src/tipos.rs", "src/camadas.rs",
|
| 144 |
+
"src/jepa/mod.rs", "src/pensamento/mod.rs",
|
| 145 |
+
"src/moe_redirecionavel/mod.rs", "src/python/mod.rs",
|
| 146 |
+
"src/fnh.rs", "src/verificador.rs",
|
| 147 |
+
"scripts/75_cli.py", "scripts/76_telemetria_v010.py",
|
| 148 |
+
"scripts/77_suite_v010.py", "scripts/78_publicar_v010.py",
|
| 149 |
+
"docs/memorias/r-35-analise-provas-qwen-v010.md",
|
| 150 |
+
"resultados/suite_v010_final.json",
|
| 151 |
+
"resultados/telemetria_v010.json",
|
| 152 |
+
"resultados/fase_a_v010.json", "resultados/fase_b_v010.json",
|
| 153 |
+
"resultados/treino_v090.json"]
|
| 154 |
+
faltando = [c for c in chaves if c not in files]
|
| 155 |
+
assert not faltando, f"faltando: {faltando}"
|
| 156 |
+
velhos = [m for m in MONOLITOS_VELHOS if m in files]
|
| 157 |
+
assert not velhos, f"monólitos velhos ainda no repo: {velhos}"
|
| 158 |
+
print(f"repo: {len(files)} arquivos | 0 estados | módulos-chave OK | "
|
| 159 |
+
f"monólitos velhos removidos")
|
| 160 |
+
|
| 161 |
+
# ---------------- 5) varredura REMOTA do token ----------------
|
| 162 |
+
print("\n=== 5) varredura REMOTA do valor do token ===")
|
| 163 |
+
with tempfile.TemporaryDirectory() as tmp:
|
| 164 |
+
achados_remotos = []
|
| 165 |
+
for f in files:
|
| 166 |
+
if f.endswith((".py", ".rs", ".md", ".json", ".toml", ".sh", ".cfg",
|
| 167 |
+
".txt", ".jsonl")):
|
| 168 |
+
try:
|
| 169 |
+
p = api.hf_hub_download(repo_id=repo, repo_type="model",
|
| 170 |
+
filename=f, local_dir=tmp)
|
| 171 |
+
with open(p, "rb") as fh:
|
| 172 |
+
if token.encode() in fh.read():
|
| 173 |
+
achados_remotos.append(f)
|
| 174 |
+
except Exception:
|
| 175 |
+
pass
|
| 176 |
+
if achados_remotos:
|
| 177 |
+
print(f"ERRO: token remoto em: {achados_remotos}"); sys.exit(1)
|
| 178 |
+
print("varredura remota: limpa")
|
| 179 |
+
|
| 180 |
+
# ---------------- 6) apaga estados de treino LOCAIS ----------------
|
| 181 |
+
print("\n=== 6) estados de treino locais (checkpoints + manifestos) ===")
|
| 182 |
+
apagados = 0
|
| 183 |
+
for nome in sorted(NOMES_ESTADO):
|
| 184 |
+
for sufixo in ("", ".manifest.json"):
|
| 185 |
+
p = os.path.join(pasta, "resultados", nome + sufixo)
|
| 186 |
+
if os.path.exists(p):
|
| 187 |
+
os.remove(p)
|
| 188 |
+
apagados += 1
|
| 189 |
+
print(f" apagado: resultados/{nome}{sufixo}")
|
| 190 |
+
print(f"locais apagados: {apagados}")
|
| 191 |
+
print("\nPUBLICAÇÃO v0.10.0 CONCLUÍDA")
|
| 192 |
+
|
| 193 |
+
if __name__ == "__main__":
|
| 194 |
+
main()
|
src/camadas.rs
CHANGED
|
@@ -136,3 +136,165 @@ impl Swiglu {
|
|
| 136 |
[self.w1.vars(), self.w3.vars(), self.w2.vars()].concat()
|
| 137 |
}
|
| 138 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
[self.w1.vars(), self.w3.vars(), self.w2.vars()].concat()
|
| 137 |
}
|
| 138 |
}
|
| 139 |
+
|
| 140 |
+
// ===========================================================================
|
| 141 |
+
// v0.10.0 — Polimorfismo de camadas (crítica Qwen3.8 lote 3, proposta 1,
|
| 142 |
+
// CORRIGIDA; r-35 §3). A proposta original tinha três problemas:
|
| 143 |
+
//
|
| 144 |
+
// (a) a PREMISSA era falsa: `controlador.rs` NÃO referencia Fnh/Moe/Jepa
|
| 145 |
+
// (verificado por busca no fonte — o controlador regula
|
| 146 |
+
// hiperparâmetros, não despacha camadas);
|
| 147 |
+
// (b) `parametros() -> Vec<&Tensor>` QUEBRA o otimizador: a partir de
|
| 148 |
+
// `&Tensor` não se recupera a `Var` para atualização in-place — a
|
| 149 |
+
// assinatura correta devolve `Vec<Var>`;
|
| 150 |
+
// (c) `gate_ativacao` na interface-base viola a segregação de
|
| 151 |
+
// interface: nem toda camada tem gate de punição. Movida para o
|
| 152 |
+
// trait `CamadaHomeostatica` (implantável só por quem tem gate).
|
| 153 |
+
//
|
| 154 |
+
// A "prova" da proposta ("composição de funções lineares e ativações
|
| 155 |
+
// preserva a dimensionalidade do batch") também está mal tipada: D_in →
|
| 156 |
+
// D_out PODE mudar (é o que uma projeção faz). O invariante correto:
|
| 157 |
+
//
|
| 158 |
+
// **Teo CK-1 (invariante de composição).** Se f: R^{B×T×D1} → R^{B×T×D2}
|
| 159 |
+
// e g: R^{B×T×D2} → R^{B×T×D3} operam apenas (i) mapas lineares na
|
| 160 |
+
// ÚLTIMA dimensão (via reshape (B·T, D) → (B·T, D') → reshape) e (ii)
|
| 161 |
+
// ativações elemento-a-elemento, então g∘f: R^{B×T×D1} → R^{B×T×D3} —
|
| 162 |
+
// as dimensões de lote B e tempo T são preservadas por toda a composição.
|
| 163 |
+
// Prova: o reshape é bijeção canônica R^{B×T×D} ≅ R^{(B·T)·D} que fixa
|
| 164 |
+
// o produto B·T; matmul por W ∈ R^{D'×D} age à direita (índice de
|
| 165 |
+
// coluna) e não permuta as linhas; ativações elemento-a-elemento agem
|
| 166 |
+
// coordenada-a-coordenada. Logo cada estágio devolve exatamente B·T
|
| 167 |
+
// linhas, e o reshape inverso restaura (B, T). ∎
|
| 168 |
+
// ===========================================================================
|
| 169 |
+
|
| 170 |
+
/// Camada com forward canônico R^{B×T×D_in} → R^{B×T×D_out} (Teo CK-1).
|
| 171 |
+
pub trait CamadaKhtst {
|
| 172 |
+
/// Forward preservando as dimensões de lote/tempo (B, T).
|
| 173 |
+
fn forward(&self, x: &Tensor) -> Result<Tensor>;
|
| 174 |
+
/// Parâmetros treináveis como `Var` — o otimizador atualiza in-place
|
| 175 |
+
/// (correção (b): `Vec<&Tensor>` romperia o SGD).
|
| 176 |
+
fn parametros(&self) -> Vec<Var>;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
/// Camada com gate de punição homeostático (segregação, correção (c)).
|
| 180 |
+
/// g = max(0, 2σ((π − θ_π)/τ_g) − 1) — zona morta exata (Teo F-3).
|
| 181 |
+
pub trait CamadaHomeostatica {
|
| 182 |
+
fn gate_ativacao(&self, punicao: &crate::tipos::Probabilidade)
|
| 183 |
+
-> crate::tipos::GateAtivacao;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
impl CamadaKhtst for Linear {
|
| 187 |
+
fn forward(&self, x: &Tensor) -> Result<Tensor> {
|
| 188 |
+
self.fwd(x)
|
| 189 |
+
}
|
| 190 |
+
fn parametros(&self) -> Vec<Var> {
|
| 191 |
+
self.vars()
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
impl CamadaKhtst for crate::jepa::MlpJepa {
|
| 196 |
+
fn forward(&self, x: &Tensor) -> Result<Tensor> {
|
| 197 |
+
self.fwd(x)
|
| 198 |
+
}
|
| 199 |
+
fn parametros(&self) -> Vec<Var> {
|
| 200 |
+
[self.l1.vars(), self.l2.vars()].concat()
|
| 201 |
+
}
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
/// Adaptador zero-custo que CAPTURA o papel (codificador/decodificador)
|
| 205 |
+
/// da MoE redirecionável, expondo o forward canônico do trait SEM perder
|
| 206 |
+
/// o parâmetro: os caminhos de treino continuam usando `forward(x, papel)`
|
| 207 |
+
/// completo (com perda auxiliar e carga — nada é removido); o adaptador
|
| 208 |
+
/// serve a composição heterogênea de INFERÊNCIA.
|
| 209 |
+
pub struct MoeComPapel<'a> {
|
| 210 |
+
pub moe: &'a crate::moe_redirecionavel::MoeRedirecionavel,
|
| 211 |
+
pub papel: crate::moe_redirecionavel::Papel,
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
impl CamadaKhtst for MoeComPapel<'_> {
|
| 215 |
+
fn forward(&self, x: &Tensor) -> Result<Tensor> {
|
| 216 |
+
Ok(self.moe.forward(x, self.papel)?.saida)
|
| 217 |
+
}
|
| 218 |
+
fn parametros(&self) -> Vec<Var> {
|
| 219 |
+
self.moe.vars()
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
#[cfg(test)]
|
| 224 |
+
mod testes_camada_khtst {
|
| 225 |
+
use super::*;
|
| 226 |
+
|
| 227 |
+
/// CK-1a — composição heterogênea `Vec<&dyn CamadaKhtst>` preserva
|
| 228 |
+
/// B×T (2, 3) através de Linear→MlpJepa (Teo CK-1) e o adaptador MoE.
|
| 229 |
+
#[test]
|
| 230 |
+
fn ck1a_composicao_heterogenea_preserva_bt() {
|
| 231 |
+
let dev = Device::Cpu;
|
| 232 |
+
let (b, t, d) = (2usize, 3usize, 8usize);
|
| 233 |
+
let lin = Linear::novo(d, 16, 0.02, &dev).unwrap();
|
| 234 |
+
let mlp = crate::jepa::MlpJepa::novo(16, 24, 32, &dev).unwrap();
|
| 235 |
+
let x0 = Tensor::randn(0f32, 1f32, (b, t, d), &dev).unwrap();
|
| 236 |
+
|
| 237 |
+
let camadas: Vec<&dyn CamadaKhtst> = vec![&lin, &mlp];
|
| 238 |
+
let mut x = x0.clone();
|
| 239 |
+
for c in &camadas {
|
| 240 |
+
x = c.forward(&x).unwrap();
|
| 241 |
+
}
|
| 242 |
+
assert_eq!(x.dims(), &[b, t, 32], "B×T preservado (Teo CK-1)");
|
| 243 |
+
|
| 244 |
+
// adaptador MoE sobre o achatado (B·T, D) — forward canônico
|
| 245 |
+
let moe = crate::moe_redirecionavel::MoeRedirecionavel::novo(32, 4, 2, 7, &dev).unwrap();
|
| 246 |
+
let adapt = MoeComPapel {
|
| 247 |
+
moe: &moe,
|
| 248 |
+
papel: crate::moe_redirecionavel::Papel::Codificador,
|
| 249 |
+
};
|
| 250 |
+
let x2d = x.reshape((b * t, 32)).unwrap();
|
| 251 |
+
let y = <MoeComPapel as CamadaKhtst>::forward(&adapt, &x2d).unwrap();
|
| 252 |
+
assert_eq!(y.dims(), &[b * t, 32]);
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
/// CK-1b — `parametros()` devolve `Var` NÃO-VAZIAS (otimizável).
|
| 256 |
+
#[test]
|
| 257 |
+
fn ck1b_parametros_nao_vazios() {
|
| 258 |
+
let dev = Device::Cpu;
|
| 259 |
+
let lin = Linear::novo(8, 16, 0.02, &dev).unwrap();
|
| 260 |
+
let mlp = crate::jepa::MlpJepa::novo(16, 24, 32, &dev).unwrap();
|
| 261 |
+
let moe = crate::moe_redirecionavel::MoeRedirecionavel::novo(32, 4, 2, 7, &dev).unwrap();
|
| 262 |
+
let adapt = MoeComPapel { moe: &moe, papel: crate::moe_redirecionavel::Papel::Codificador };
|
| 263 |
+
for (nome, n) in [("linear", lin.parametros().len()),
|
| 264 |
+
("mlp", mlp.parametros().len()),
|
| 265 |
+
("moe", adapt.parametros().len())] {
|
| 266 |
+
assert!(n > 0, "{nome} deveria ter parâmetros");
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
/// CK-1c — o adaptador delega BIT-A-BIT para moe.forward(x, papel).
|
| 271 |
+
#[test]
|
| 272 |
+
fn ck1c_adaptador_bit_a_bit() {
|
| 273 |
+
let dev = Device::Cpu;
|
| 274 |
+
let moe = crate::moe_redirecionavel::MoeRedirecionavel::novo(16, 4, 2, 11, &dev).unwrap();
|
| 275 |
+
let x = Tensor::randn(0f32, 1f32, (5, 16), &dev).unwrap();
|
| 276 |
+
let direto = moe.forward(&x, crate::moe_redirecionavel::Papel::Codificador).unwrap();
|
| 277 |
+
let adapt = MoeComPapel { moe: &moe, papel: crate::moe_redirecionavel::Papel::Codificador };
|
| 278 |
+
let via_trait = <MoeComPapel as CamadaKhtst>::forward(&adapt, &x).unwrap();
|
| 279 |
+
let a = direto.saida.flatten_all().unwrap().to_vec1::<f32>().unwrap();
|
| 280 |
+
let bvia = via_trait.flatten_all().unwrap().to_vec1::<f32>().unwrap();
|
| 281 |
+
assert_eq!(a.len(), bvia.len());
|
| 282 |
+
for (i, (x1, x2)) in a.iter().zip(bvia.iter()).enumerate() {
|
| 283 |
+
assert_eq!(x1.to_bits(), x2.to_bits(), "elem {i} difere");
|
| 284 |
+
}
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
/// CK-2a — despacho homeostático heterogêneo: zona morta exata em
|
| 288 |
+
/// π < θ_π e ativação > 0 em π alto (Teo F-3 via fronteira tipada).
|
| 289 |
+
#[test]
|
| 290 |
+
fn ck2a_despacho_homeostatico() {
|
| 291 |
+
let gate = crate::fnh::GatePunicao::default();
|
| 292 |
+
let camadas: Vec<&dyn CamadaHomeostatica> = vec![&gate];
|
| 293 |
+
let baixa = crate::tipos::Probabilidade::novo(0.2).unwrap();
|
| 294 |
+
let alta = crate::tipos::Probabilidade::novo(0.95).unwrap();
|
| 295 |
+
for c in &camadas {
|
| 296 |
+
assert_eq!(c.gate_ativacao(&baixa).valor(), 0.0, "zona morta exata");
|
| 297 |
+
assert!(c.gate_ativacao(&alta).valor() > 0.0, "gate abre sob punição alta");
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
}
|
src/checkpoint.rs
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Checkpoints atômicos com prova de integridade criptográfica (v0.10.0).
|
| 2 |
+
//!
|
| 3 |
+
//! **Crítica Qwen3.8 lote 3, nº 3 ("Checkpointing Não-Atômico") — ACEITA
|
| 4 |
+
//! COM CORREÇÕES (r-35 §4).** A proposta original tinha três falhas:
|
| 5 |
+
//!
|
| 6 |
+
//! 1. **Destruição do estado anterior:** `File::create(caminho_pesos)`
|
| 7 |
+
//! trunca o checkpoint ANTERIOR antes de escrever o novo — uma queda
|
| 8 |
+
//! de energia no meio da escrita perde AMBOS. A atomicidade real exige
|
| 9 |
+
//! escrita em `.tmp` + `rename(2)` (o POSIX garante troca atômica).
|
| 10 |
+
//! 2. **Manifesto sem barreira:** `fs::write(caminho_meta, ...)` sem
|
| 11 |
+
//! `sync_all` — o manifesto pode não atingir o disco quando os pesos
|
| 12 |
+
//! já atingiram (ou vice-versa), violando a própria ordenação alegada.
|
| 13 |
+
//! 3. **Dependência nova (bincode) + `unwrap()`** em caminho de I/O.
|
| 14 |
+
//!
|
| 15 |
+
//! Esta implementação: SHA-256 **puro em Rust** (FIPS 180-4, sem crates
|
| 16 |
+
//! externas — crates.io inacessível no ambiente), escrita tmp→fsync→
|
| 17 |
+
//! rename→fsync(dir) para AMBOS os arquivos, e manifesto lado-a-lado
|
| 18 |
+
//! `{caminho}.manifest.json` com hash + tamanho.
|
| 19 |
+
//!
|
| 20 |
+
//! **Teo CP-1 (atomicidade da troca).** `rename(2)` é atômico em relação
|
| 21 |
+
//! a leitores concorrentes e crashes: em qualquer instante o caminho
|
| 22 |
+
//! resolve OU para o arquivo antigo OU para o novo, nunca uma mistura.
|
| 23 |
+
//! Logo um crash em qualquer ponto deixa o caminho final = checkpoint
|
| 24 |
+
//! antigo (crash antes do rename) ou novo (depois). ∎ (garantia POSIX)
|
| 25 |
+
//!
|
| 26 |
+
//! **Teo CP-2 (durabilidade ordenada).** Com a disciplina
|
| 27 |
+
//! `write(W.tmp) → fsync(W.tmp) → rename(W) → write(M.tmp) →
|
| 28 |
+
//! fsync(M.tmp) → rename(M) → fsync(dir)`, após reboot: se `M` existe,
|
| 29 |
+
//! então o rename de `W` foi durável (executado e journalizado antes) e
|
| 30 |
+
//! os blocos de dados de `W` foram forçados por `fsync(W.tmp)` — portanto
|
| 31 |
+
//! `SHA-256(W) = M.hash` (a menos de colisão, CP-3). Se `M` não existe,
|
| 32 |
+
//! o estado é tratado como INVÁLIDO (não silenciosamente errado). ∎
|
| 33 |
+
//!
|
| 34 |
+
//! **Teo CP-3 (detecção de corrupção).** Para W' ≠ W com |W'| = |W|,
|
| 35 |
+
//! `P[SHA-256(W') = SHA-256(W)] ≤ 2^-256` (ataque direto) e, para
|
| 36 |
+
//! qualquer par distinto, ≤ 2^-128 (limite de aniversário). Truncamentos
|
| 37 |
+
//! e gravações parciais são capturados DETERMINISTICAMENTE pelo campo
|
| 38 |
+
//! `tamanho`. Nota honesta: a alegação original "matematicamente
|
| 39 |
+
//! idêntico" é exagerada — o correto é indistinguibilidade computacional
|
| 40 |
+
//! com probabilidade de colisão ≤ 2^-128. ∎
|
| 41 |
+
//!
|
| 42 |
+
//! **Compatibilidade (regra da casa: nada é removido):** checkpoints da
|
| 43 |
+
//! v0.9.0 (sem manifesto) continuam carregáveis pelos `carregar` legados;
|
| 44 |
+
//! `carregar_verificado` exige manifesto e recusa o contrário.
|
| 45 |
+
|
| 46 |
+
use candle_core::Result;
|
| 47 |
+
use std::fs::File;
|
| 48 |
+
use std::io::Write;
|
| 49 |
+
use std::path::Path;
|
| 50 |
+
|
| 51 |
+
pub const VERSAO_CHECKPOINT: &str = "0.10.0";
|
| 52 |
+
|
| 53 |
+
// ---------------------------------------------------------------------------
|
| 54 |
+
// SHA-256 — FIPS 180-4, implementação autocontida (sem dependências)
|
| 55 |
+
// ---------------------------------------------------------------------------
|
| 56 |
+
|
| 57 |
+
const K_SHA256: [u32; 64] = [
|
| 58 |
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
|
| 59 |
+
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
| 60 |
+
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
|
| 61 |
+
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
| 62 |
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
|
| 63 |
+
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
| 64 |
+
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
|
| 65 |
+
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
| 66 |
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
|
| 67 |
+
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
| 68 |
+
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
| 69 |
+
];
|
| 70 |
+
|
| 71 |
+
/// SHA-256 de `dados` — 32 bytes (FIPS 180-4 §6.2.2).
|
| 72 |
+
pub fn sha256(dados: &[u8]) -> [u8; 32] {
|
| 73 |
+
let mut h: [u32; 8] = [
|
| 74 |
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
| 75 |
+
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
| 76 |
+
];
|
| 77 |
+
// padding: 0x80, zeros, comprimento de bit big-endian em 64 bits
|
| 78 |
+
let bitlen = (dados.len() as u64).wrapping_mul(8);
|
| 79 |
+
let mut msg = dados.to_vec();
|
| 80 |
+
msg.push(0x80);
|
| 81 |
+
while msg.len() % 64 != 56 {
|
| 82 |
+
msg.push(0);
|
| 83 |
+
}
|
| 84 |
+
msg.extend_from_slice(&bitlen.to_be_bytes());
|
| 85 |
+
|
| 86 |
+
for bloco in msg.chunks_exact(64) {
|
| 87 |
+
let mut w = [0u32; 64];
|
| 88 |
+
for (i, j) in (0..64).step_by(4).enumerate() {
|
| 89 |
+
w[i] = u32::from_be_bytes([bloco[j], bloco[j + 1], bloco[j + 2], bloco[j + 3]]);
|
| 90 |
+
}
|
| 91 |
+
for t in 16..64 {
|
| 92 |
+
let s0 = w[t - 15].rotate_right(7) ^ w[t - 15].rotate_right(18) ^ (w[t - 15] >> 3);
|
| 93 |
+
let s1 = w[t - 2].rotate_right(17) ^ w[t - 2].rotate_right(19) ^ (w[t - 2] >> 10);
|
| 94 |
+
w[t] = w[t - 16]
|
| 95 |
+
.wrapping_add(s0)
|
| 96 |
+
.wrapping_add(w[t - 7])
|
| 97 |
+
.wrapping_add(s1);
|
| 98 |
+
}
|
| 99 |
+
let (mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh) =
|
| 100 |
+
(h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]);
|
| 101 |
+
for t in 0..64 {
|
| 102 |
+
let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
|
| 103 |
+
let ch = (e & f) ^ ((!e) & g);
|
| 104 |
+
let t1 = hh
|
| 105 |
+
.wrapping_add(s1)
|
| 106 |
+
.wrapping_add(ch)
|
| 107 |
+
.wrapping_add(K_SHA256[t])
|
| 108 |
+
.wrapping_add(w[t]);
|
| 109 |
+
let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
|
| 110 |
+
let maj = (a & b) ^ (a & c) ^ (b & c);
|
| 111 |
+
let t2 = s0.wrapping_add(maj);
|
| 112 |
+
hh = g; g = f; f = e; e = d.wrapping_add(t1);
|
| 113 |
+
d = c; c = b; b = a; a = t1.wrapping_add(t2);
|
| 114 |
+
}
|
| 115 |
+
h[0] = h[0].wrapping_add(a); h[1] = h[1].wrapping_add(b);
|
| 116 |
+
h[2] = h[2].wrapping_add(c); h[3] = h[3].wrapping_add(d);
|
| 117 |
+
h[4] = h[4].wrapping_add(e); h[5] = h[5].wrapping_add(f);
|
| 118 |
+
h[6] = h[6].wrapping_add(g); h[7] = h[7].wrapping_add(hh);
|
| 119 |
+
}
|
| 120 |
+
let mut saida = [0u8; 32];
|
| 121 |
+
for (i, x) in h.iter().enumerate() {
|
| 122 |
+
saida[i * 4..i * 4 + 4].copy_from_slice(&x.to_be_bytes());
|
| 123 |
+
}
|
| 124 |
+
saida
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/// SHA-256 em hexadecimal minúsculo (64 caracteres).
|
| 128 |
+
pub fn sha256_hex(dados: &[u8]) -> String {
|
| 129 |
+
sha256(dados).iter().map(|b| format!("{b:02x}")).collect()
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
// ---------------------------------------------------------------------------
|
| 133 |
+
// salva / carrega atômicos
|
| 134 |
+
// ---------------------------------------------------------------------------
|
| 135 |
+
|
| 136 |
+
/// Caminho do manifesto lado-a-lado de `caminho`.
|
| 137 |
+
pub fn caminho_manifesto(caminho: &str) -> String {
|
| 138 |
+
format!("{caminho}.manifest.json")
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
fn err_io(e: std::io::Error, ctx: &str) -> candle_core::Error {
|
| 142 |
+
candle_core::Error::Msg(format!("{ctx}: {e}"))
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/// fsync de um diretório (durabilidade do rename — POSIX). Unix apenas;
|
| 146 |
+
/// em outras plataformas é no-op documentado.
|
| 147 |
+
fn fsync_dir(dir: &Path) -> std::io::Result<()> {
|
| 148 |
+
#[cfg(unix)]
|
| 149 |
+
{
|
| 150 |
+
let d = File::open(dir)?;
|
| 151 |
+
d.sync_all()?;
|
| 152 |
+
}
|
| 153 |
+
#[cfg(not(unix))]
|
| 154 |
+
{
|
| 155 |
+
let _ = dir;
|
| 156 |
+
}
|
| 157 |
+
Ok(())
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
/// Escrita atômica de `conteudo` em `caminho` com fsync + rename e
|
| 161 |
+
/// manifesto `{caminho}.manifest.json` (hash SHA-256 + tamanho).
|
| 162 |
+
///
|
| 163 |
+
/// Ordem (Teo CP-2): write(tmp) → fsync(tmp) → rename(final) para o
|
| 164 |
+
/// conteúdo e para o manifesto; fsync(dir) ao final.
|
| 165 |
+
pub fn salvar_atomico(caminho: &str, conteudo: &str) -> Result<()> {
|
| 166 |
+
let dados = conteudo.as_bytes();
|
| 167 |
+
let hash = sha256_hex(dados);
|
| 168 |
+
let tmp = format!("{caminho}.tmp");
|
| 169 |
+
|
| 170 |
+
// 1) conteúdo: tmp → fsync → rename (Teo CP-1: troca atômica)
|
| 171 |
+
{
|
| 172 |
+
let mut f = File::create(&tmp).map_err(|e| err_io(e, "checkpoint: criar tmp"))?;
|
| 173 |
+
f.write_all(dados).map_err(|e| err_io(e, "checkpoint: escrever tmp"))?;
|
| 174 |
+
f.sync_all().map_err(|e| err_io(e, "checkpoint: fsync tmp"))?;
|
| 175 |
+
}
|
| 176 |
+
std::fs::rename(&tmp, caminho).map_err(|e| err_io(e, "checkpoint: rename"))?;
|
| 177 |
+
|
| 178 |
+
// 2) manifesto: mesma disciplina (a proposta original usava fs::write
|
| 179 |
+
// simples — sem barreira, o manifesto pode perder-se sozinho)
|
| 180 |
+
let manifest = serde_json::json!({
|
| 181 |
+
"versao": VERSAO_CHECKPOINT,
|
| 182 |
+
"algoritmo": "sha256",
|
| 183 |
+
"hash_sha256": hash,
|
| 184 |
+
"tamanho": dados.len(),
|
| 185 |
+
});
|
| 186 |
+
let mtxt = serde_json::to_string(&manifest)
|
| 187 |
+
.map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))?;
|
| 188 |
+
let mtmp = format!("{}.manifest.json.tmp", caminho);
|
| 189 |
+
{
|
| 190 |
+
let mut f = File::create(&mtmp).map_err(|e| err_io(e, "manifesto: criar tmp"))?;
|
| 191 |
+
f.write_all(mtxt.as_bytes()).map_err(|e| err_io(e, "manifesto: escrever tmp"))?;
|
| 192 |
+
f.sync_all().map_err(|e| err_io(e, "manifesto: fsync tmp"))?;
|
| 193 |
+
}
|
| 194 |
+
std::fs::rename(&mtmp, caminho_manifesto(caminho))
|
| 195 |
+
.map_err(|e| err_io(e, "manifesto: rename"))?;
|
| 196 |
+
|
| 197 |
+
// 3) durabilidade dos renames (POSIX: rename journalizado no dir)
|
| 198 |
+
if let Some(pai) = Path::new(caminho).parent() {
|
| 199 |
+
fsync_dir(pai).map_err(|e| err_io(e, "checkpoint: fsync dir"))?;
|
| 200 |
+
}
|
| 201 |
+
Ok(())
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
/// Lê `caminho` verificando o manifesto (hash + tamanho). Recusa
|
| 205 |
+
/// manifesto ausente ou divergente — Teo CP-3.
|
| 206 |
+
pub fn carregar_verificado(caminho: &str) -> Result<String> {
|
| 207 |
+
let dados = std::fs::read(caminho).map_err(|e| err_io(e, "checkpoint: ler"))?;
|
| 208 |
+
let mtxt = std::fs::read_to_string(caminho_manifesto(caminho))
|
| 209 |
+
.map_err(|_| candle_core::Error::Msg(format!(
|
| 210 |
+
"checkpoint sem manifesto (legado v0.9.0?): {caminho}")))?;
|
| 211 |
+
let m: serde_json::Value = serde_json::from_str(&mtxt)
|
| 212 |
+
.map_err(|e| candle_core::Error::Msg(format!("manifesto ilegível: {e}")))?;
|
| 213 |
+
let hash_esp = m["hash_sha256"].as_str().unwrap_or("");
|
| 214 |
+
let tam_esp = m["tamanho"].as_u64().unwrap_or(u64::MAX);
|
| 215 |
+
let hash_rec = sha256_hex(&dados);
|
| 216 |
+
if (dados.len() as u64) != tam_esp {
|
| 217 |
+
candle_core::bail!(
|
| 218 |
+
"checkpoint corrompido: tamanho {} ��� manifesto {} ({caminho})",
|
| 219 |
+
dados.len(), tam_esp);
|
| 220 |
+
}
|
| 221 |
+
if hash_rec != hash_esp {
|
| 222 |
+
candle_core::bail!(
|
| 223 |
+
"checkpoint corrompido: SHA-256 divergente ({caminho})");
|
| 224 |
+
}
|
| 225 |
+
String::from_utf8(dados)
|
| 226 |
+
.map_err(|e| candle_core::Error::Msg(format!("utf8: {e}")))
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
/// Verificação sem exceção (para uso em Python/CLI antes de resume).
|
| 230 |
+
pub fn validar(caminho: &str) -> bool {
|
| 231 |
+
carregar_verificado(caminho).is_ok()
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
#[cfg(test)]
|
| 235 |
+
mod testes_checkpoint {
|
| 236 |
+
use super::*;
|
| 237 |
+
|
| 238 |
+
fn tmp_dir(nome: &str) -> std::path::PathBuf {
|
| 239 |
+
let d = std::env::temp_dir().join(format!("khtst_cp_{nome}_{}", std::process::id()));
|
| 240 |
+
let _ = std::fs::remove_dir_all(&d);
|
| 241 |
+
std::fs::create_dir_all(&d).unwrap();
|
| 242 |
+
d
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
/// CP-0a — SHA-256 contra os vetores oficiais FIPS 180-4.
|
| 246 |
+
#[test]
|
| 247 |
+
fn cp0a_sha256_vetores_fips() {
|
| 248 |
+
assert_eq!(
|
| 249 |
+
sha256_hex(b""),
|
| 250 |
+
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
|
| 251 |
+
assert_eq!(
|
| 252 |
+
sha256_hex(b"abc"),
|
| 253 |
+
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
|
| 254 |
+
assert_eq!(
|
| 255 |
+
sha256_hex(b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"),
|
| 256 |
+
"248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1");
|
| 257 |
+
// vetor de 4 blocos (256 bytes) — exercita o laço de blocos
|
| 258 |
+
let longo = "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmn".repeat(4);
|
| 259 |
+
assert_eq!(sha256_hex(longo.as_bytes()).len(), 64);
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
/// CP-1a — salvar produz final + manifesto; tmp não permanece;
|
| 263 |
+
/// carregar_verificado devolve o conteúdo bit-a-bit.
|
| 264 |
+
#[test]
|
| 265 |
+
fn cp1a_salvar_carregar_roundtrip() {
|
| 266 |
+
let d = tmp_dir("rt");
|
| 267 |
+
let p = d.join("ck.json");
|
| 268 |
+
salvar_atomico(p.to_str().unwrap(), "{\"v0\":[1.0,2.0]}").unwrap();
|
| 269 |
+
assert!(p.exists());
|
| 270 |
+
assert!(d.join("ck.json.manifest.json").exists());
|
| 271 |
+
assert!(!d.join("ck.json.tmp").exists());
|
| 272 |
+
assert!(!d.join("ck.json.manifest.json.tmp").exists());
|
| 273 |
+
assert_eq!(carregar_verificado(p.to_str().unwrap()).unwrap(),
|
| 274 |
+
"{\"v0\":[1.0,2.0]}");
|
| 275 |
+
let _ = std::fs::remove_dir_all(&d);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
/// CP-3a — adulteração de 1 byte é detectada (hash divergente).
|
| 279 |
+
#[test]
|
| 280 |
+
fn cp3a_adulteracao_detectada() {
|
| 281 |
+
let d = tmp_dir("t");
|
| 282 |
+
let p = d.join("ck.json");
|
| 283 |
+
salvar_atomico(p.to_str().unwrap(), "conteudo original").unwrap();
|
| 284 |
+
std::fs::write(&p, "conteudo originalX").unwrap();
|
| 285 |
+
assert!(carregar_verificado(p.to_str().unwrap()).is_err());
|
| 286 |
+
assert!(!validar(p.to_str().unwrap()));
|
| 287 |
+
let _ = std::fs::remove_dir_all(&d);
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
/// CP-3b — truncamento detectado pelo tamanho (determinístico).
|
| 291 |
+
#[test]
|
| 292 |
+
fn cp3b_truncamento_detectado() {
|
| 293 |
+
let d = tmp_dir("tr");
|
| 294 |
+
let p = d.join("ck.json");
|
| 295 |
+
salvar_atomico(p.to_str().unwrap(), "0123456789").unwrap();
|
| 296 |
+
std::fs::write(&p, "0123").unwrap();
|
| 297 |
+
assert!(carregar_verificado(p.to_str().unwrap()).is_err());
|
| 298 |
+
let _ = std::fs::remove_dir_all(&d);
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
/// CP-4a — manifesto ausente ⇒ recusa (estado legado é do `carregar`).
|
| 302 |
+
#[test]
|
| 303 |
+
fn cp4a_manifesto_ausente_recusado() {
|
| 304 |
+
let d = tmp_dir("leg");
|
| 305 |
+
let p = d.join("ck.json");
|
| 306 |
+
salvar_atomico(p.to_str().unwrap(), "x").unwrap();
|
| 307 |
+
std::fs::remove_file(d.join("ck.json.manifest.json")).unwrap();
|
| 308 |
+
assert!(carregar_verificado(p.to_str().unwrap()).is_err());
|
| 309 |
+
let _ = std::fs::remove_dir_all(&d);
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
/// CP-1b — falha de escrita PRESERVA o checkpoint anterior
|
| 313 |
+
/// (atomicidade prática: dir somente-leitura impede o create do tmp).
|
| 314 |
+
#[test]
|
| 315 |
+
fn cp1b_falha_preserva_anterior() {
|
| 316 |
+
let d = tmp_dir("fa");
|
| 317 |
+
let p = d.join("ck.json");
|
| 318 |
+
salvar_atomico(p.to_str().unwrap(), "estado_bom").unwrap();
|
| 319 |
+
// dir somente-leitura: File::create(tmp) falha
|
| 320 |
+
let mut perms = std::fs::metadata(&d).unwrap().permissions();
|
| 321 |
+
#[allow(clippy::unnecessary_cast)]
|
| 322 |
+
{
|
| 323 |
+
use std::os::unix::fs::PermissionsExt;
|
| 324 |
+
perms.set_mode(0o555);
|
| 325 |
+
}
|
| 326 |
+
std::fs::set_permissions(&d, perms).unwrap();
|
| 327 |
+
let r = salvar_atomico(p.to_str().unwrap(), "estado_novo");
|
| 328 |
+
let mut perms2 = std::fs::metadata(&d).unwrap().permissions();
|
| 329 |
+
#[allow(clippy::unnecessary_cast)]
|
| 330 |
+
{
|
| 331 |
+
use std::os::unix::fs::PermissionsExt;
|
| 332 |
+
perms2.set_mode(0o755);
|
| 333 |
+
}
|
| 334 |
+
std::fs::set_permissions(&d, perms2).unwrap();
|
| 335 |
+
assert!(r.is_err(), "escrita deveria falhar em dir somente-leitura");
|
| 336 |
+
// o anterior permanece ÍNTEGRO e válido (Teo CP-1)
|
| 337 |
+
assert_eq!(carregar_verificado(p.to_str().unwrap()).unwrap(), "estado_bom");
|
| 338 |
+
let _ = std::fs::remove_dir_all(&d);
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
/// CP-5a — manifesto trocado entre arquivos (hash de outro) recusa.
|
| 342 |
+
#[test]
|
| 343 |
+
fn cp5a_manifesto_de_outro_recusado() {
|
| 344 |
+
let d = tmp_dir("sw");
|
| 345 |
+
let a = d.join("a.json");
|
| 346 |
+
let b = d.join("b.json");
|
| 347 |
+
salvar_atomico(a.to_str().unwrap(), "AAAA").unwrap();
|
| 348 |
+
salvar_atomico(b.to_str().unwrap(), "BBBB").unwrap();
|
| 349 |
+
std::fs::copy(d.join("b.json.manifest.json"), d.join("a.json.manifest.json")).unwrap();
|
| 350 |
+
assert!(carregar_verificado(a.to_str().unwrap()).is_err());
|
| 351 |
+
let _ = std::fs::remove_dir_all(&d);
|
| 352 |
+
}
|
| 353 |
+
}
|
src/fnh.rs
CHANGED
|
@@ -570,6 +570,13 @@ impl GatePunicao {
|
|
| 570 |
}
|
| 571 |
}
|
| 572 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
/// Camada FNH — uma hipótese flexível com rotações aprendíveis, suavização de
|
| 574 |
/// canais e ativação flexível, GATADA pela punição do treino (r-28 §4.1).
|
| 575 |
pub struct CamadaFNH {
|
|
|
|
| 570 |
}
|
| 571 |
}
|
| 572 |
|
| 573 |
+
impl crate::camadas::CamadaHomeostatica for GatePunicao {
|
| 574 |
+
fn gate_ativacao(&self, punicao: &crate::tipos::Probabilidade)
|
| 575 |
+
-> crate::tipos::GateAtivacao {
|
| 576 |
+
self.valor_tipado(punicao)
|
| 577 |
+
}
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
/// Camada FNH — uma hipótese flexível com rotações aprendíveis, suavização de
|
| 581 |
/// canais e ativação flexível, GATADA pela punição do treino (r-28 §4.1).
|
| 582 |
pub struct CamadaFNH {
|
src/gerador.rs
CHANGED
|
@@ -246,18 +246,27 @@ impl GeradorConceitos {
|
|
| 246 |
}
|
| 247 |
let txt = serde_json::to_string(&serde_json::Value::Object(obj))
|
| 248 |
.map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))?;
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
}
|
| 254 |
|
| 255 |
/// Carrega um checkpoint salvo por `salvar` (mesma ordem de construção
|
| 256 |
/// de `novo` ⇒ índices de variáveis casam 1-a-1 — ajuste fino honesto).
|
|
|
|
| 257 |
pub fn carregar(&mut self, caminho: &str) -> Result<()> {
|
| 258 |
let txt = std::fs::read_to_string(caminho)
|
| 259 |
.map_err(|e| candle_core::Error::Msg(format!("io: {e}")))?;
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
.map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))?;
|
| 262 |
let vars = self.vars();
|
| 263 |
for (i, v) in vars.iter().enumerate() {
|
|
|
|
| 246 |
}
|
| 247 |
let txt = serde_json::to_string(&serde_json::Value::Object(obj))
|
| 248 |
.map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))?;
|
| 249 |
+
crate::checkpoint::salvar_atomico(caminho, &txt)
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
/// Carrega com verificação de integridade (manifesto SHA-256 — v0.10.0).
|
| 253 |
+
/// Recusa checkpoints corrompidos/legados-sem-manifesto (Teo CP-3).
|
| 254 |
+
pub fn carregar_verificado(&mut self, caminho: &str) -> Result<()> {
|
| 255 |
+
let txt = crate::checkpoint::carregar_verificado(caminho)?;
|
| 256 |
+
self.carregar_de_txt(&txt)
|
| 257 |
}
|
| 258 |
|
| 259 |
/// Carrega um checkpoint salvo por `salvar` (mesma ordem de construção
|
| 260 |
/// de `novo` ⇒ índices de variáveis casam 1-a-1 — ajuste fino honesto).
|
| 261 |
+
/// Caminho LEGADO: sem verificação de manifesto (compatibilidade v0.9.0).
|
| 262 |
pub fn carregar(&mut self, caminho: &str) -> Result<()> {
|
| 263 |
let txt = std::fs::read_to_string(caminho)
|
| 264 |
.map_err(|e| candle_core::Error::Msg(format!("io: {e}")))?;
|
| 265 |
+
self.carregar_de_txt(&txt)
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
fn carregar_de_txt(&mut self, txt: &str) -> Result<()> {
|
| 269 |
+
let obj: serde_json::Map<String, serde_json::Value> = serde_json::from_str(txt)
|
| 270 |
.map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))?;
|
| 271 |
let vars = self.vars();
|
| 272 |
for (i, v) in vars.iter().enumerate() {
|
src/jepa/mascaras.rs
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `jepa::mascaras` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
// ---------------------------------------------------------------------------
|
| 8 |
+
// Parte 2 — máscaras
|
| 9 |
+
// ---------------------------------------------------------------------------
|
| 10 |
+
|
| 11 |
+
/// Máscara de blocos 2D (Image-JEPA): grade (h, w) de patches; sorteia
|
| 12 |
+
/// `n_blocos` retângulos aleatórios com fração alvo `frac` de patches.
|
| 13 |
+
pub fn mascara_blocos_2d(h: usize, w: usize, n_blocos: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
|
| 14 |
+
let total = h * w;
|
| 15 |
+
let alvo = (total as f64 * frac) as usize;
|
| 16 |
+
let mut masc = vec![false; total];
|
| 17 |
+
let mut coberto = 0usize;
|
| 18 |
+
for _ in 0..n_blocos.max(1) {
|
| 19 |
+
if coberto >= alvo {
|
| 20 |
+
break;
|
| 21 |
+
}
|
| 22 |
+
let bh = 1 + rng.inteiro(h.min(4));
|
| 23 |
+
let bw = 1 + rng.inteiro(w.min(4));
|
| 24 |
+
let y0 = rng.inteiro(h + 1 - bh);
|
| 25 |
+
let x0 = rng.inteiro(w + 1 - bw);
|
| 26 |
+
for y in y0..y0 + bh {
|
| 27 |
+
for x in x0..x0 + bw {
|
| 28 |
+
let i = y * w + x;
|
| 29 |
+
if !masc[i] {
|
| 30 |
+
masc[i] = true;
|
| 31 |
+
coberto += 1;
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
masc
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/// Máscara de tubos 3D (V-JEPA 2): grade (t, h, w); tubos = retângulos que
|
| 40 |
+
/// atravessam o tempo.
|
| 41 |
+
pub fn mascara_tubos_3d(t: usize, h: usize, w: usize, n_tubos: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
|
| 42 |
+
let total = t * h * w;
|
| 43 |
+
let alvo = (total as f64 * frac) as usize;
|
| 44 |
+
let mut masc = vec![false; total];
|
| 45 |
+
let mut coberto = 0usize;
|
| 46 |
+
for _ in 0..n_tubos.max(1) {
|
| 47 |
+
if coberto >= alvo {
|
| 48 |
+
break;
|
| 49 |
+
}
|
| 50 |
+
let bh = 1 + rng.inteiro(h.min(3));
|
| 51 |
+
let bw = 1 + rng.inteiro(w.min(3));
|
| 52 |
+
let y0 = rng.inteiro(h + 1 - bh);
|
| 53 |
+
let x0 = rng.inteiro(w + 1 - bw);
|
| 54 |
+
for tt in 0..t {
|
| 55 |
+
for y in y0..y0 + bh {
|
| 56 |
+
for x in x0..x0 + bw {
|
| 57 |
+
let i = (tt * h + y) * w + x;
|
| 58 |
+
if !masc[i] {
|
| 59 |
+
masc[i] = true;
|
| 60 |
+
coberto += 1;
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
masc
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/// Máscara de bandas (Audio-JEPA): grade (f, tau) — bandas de frequência
|
| 70 |
+
/// horizontais com extensão temporal.
|
| 71 |
+
pub fn mascara_bandas(f: usize, tau: usize, n_bandas: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
|
| 72 |
+
mascara_blocos_2d(f, tau, n_bandas, frac, rng) // blocos na grade (f, tau)
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
/// Máscara de blocos 3D (3D-JEPA): grade (x, y, z) de voxels.
|
| 76 |
+
pub fn mascara_blocos_3d(x: usize, y: usize, z: usize, n_blocos: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
|
| 77 |
+
mascara_tubos_3d(x, y, z, n_blocos, frac, rng)
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/// Máscara de segmentos canal×tempo (Brain-JEPA): canais inteiros OU
|
| 81 |
+
/// janelas temporais (aleatório por chamada).
|
| 82 |
+
pub fn mascara_segmentos(c: usize, t: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
|
| 83 |
+
let total = c * t;
|
| 84 |
+
let alvo = (total as f64 * frac) as usize;
|
| 85 |
+
let mut masc = vec![false; total];
|
| 86 |
+
if rng.inteiro(2) == 0 {
|
| 87 |
+
// canais inteiros
|
| 88 |
+
let mut coberto = 0;
|
| 89 |
+
let mut canais: Vec<usize> = (0..c).collect();
|
| 90 |
+
// embaralhar
|
| 91 |
+
for i in (1..canais.len()).rev() {
|
| 92 |
+
let j = rng.inteiro(i + 1);
|
| 93 |
+
canais.swap(i, j);
|
| 94 |
+
}
|
| 95 |
+
for &ch in canais.iter() {
|
| 96 |
+
if coberto >= alvo {
|
| 97 |
+
break;
|
| 98 |
+
}
|
| 99 |
+
for tt in 0..t {
|
| 100 |
+
masc[ch * t + tt] = true;
|
| 101 |
+
coberto += 1;
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
} else {
|
| 105 |
+
// janelas temporais
|
| 106 |
+
let mut coberto = 0;
|
| 107 |
+
let mut t0 = 0;
|
| 108 |
+
while coberto < alvo && t0 < t {
|
| 109 |
+
let len = 1 + rng.inteiro((t / 4).max(1));
|
| 110 |
+
for ch in 0..c {
|
| 111 |
+
for tt in t0..(t0 + len).min(t) {
|
| 112 |
+
masc[ch * t + tt] = true;
|
| 113 |
+
coberto += 1;
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
t0 += len + 1;
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
masc
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/// Máscara 1D (T-JEPA): intervalos contíguos em n posições.
|
| 123 |
+
pub fn mascara_intervalos_1d(n: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
|
| 124 |
+
let alvo = (n as f64 * frac) as usize;
|
| 125 |
+
let mut masc = vec![false; n];
|
| 126 |
+
let mut coberto = 0;
|
| 127 |
+
while coberto < alvo {
|
| 128 |
+
let len = 1 + rng.inteiro((n / 8).max(1));
|
| 129 |
+
let i0 = rng.inteiro(n);
|
| 130 |
+
for i in i0..(i0 + len).min(n) {
|
| 131 |
+
if !masc[i] {
|
| 132 |
+
masc[i] = true;
|
| 133 |
+
coberto += 1;
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
masc
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/// Amostragem dos pontos mais distantes (FPS) para Point-JEPA.
|
| 141 |
+
pub fn pontos_mais_distantes(pontos: &[Vec<f64>], k: usize, rng: &mut RngDeterministico) -> Vec<usize> {
|
| 142 |
+
let n = pontos.len();
|
| 143 |
+
if n == 0 || k == 0 {
|
| 144 |
+
return vec![];
|
| 145 |
+
}
|
| 146 |
+
let k = k.min(n);
|
| 147 |
+
let mut centros = vec![rng.inteiro(n)];
|
| 148 |
+
let mut dist_min: Vec<f64> = pontos
|
| 149 |
+
.iter()
|
| 150 |
+
.map(|p| distancia_eucl(p, &pontos[centros[0]]))
|
| 151 |
+
.collect();
|
| 152 |
+
while centros.len() < k {
|
| 153 |
+
let prox = dist_min
|
| 154 |
+
.iter()
|
| 155 |
+
.enumerate()
|
| 156 |
+
.filter(|(i, _)| !centros.contains(i))
|
| 157 |
+
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
|
| 158 |
+
.map(|(i, _)| i)
|
| 159 |
+
.unwrap_or(0);
|
| 160 |
+
centros.push(prox);
|
| 161 |
+
for (i, p) in pontos.iter().enumerate() {
|
| 162 |
+
let d = distancia_eucl(p, &pontos[prox]);
|
| 163 |
+
if d < dist_min[i] {
|
| 164 |
+
dist_min[i] = d;
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
centros
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
pub(crate) fn distancia_eucl(a: &[f64], b: &[f64]) -> f64 {
|
| 172 |
+
a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum::<f64>().sqrt()
|
| 173 |
+
}
|
src/jepa/mod.rs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Suíte JEPA — módulos: núcleo, máscaras, patches, variantes,
|
| 2 |
+
//! SonicMoE, ReplaySSM (divisão SRP v0.10.0 — r-35 §2).
|
| 3 |
+
|
| 4 |
+
//! Suíte JEPA v0.5.0 — núcleo unificado + 12 variantes + SonicMoE + ReplaySSM.
|
| 5 |
+
//!
|
| 6 |
+
//! **Núcleo (todas as variantes compartilham):**
|
| 7 |
+
//! - codificador `E_θ` (MLp SiLU), preditor `P_φ` (condicionado por posição/
|
| 8 |
+
//! ação/edição), codificador-alvo EMA `E_ξ` com `ξ ← m·ξ + (1−m)·θ`
|
| 9 |
+
//! (gradiente cortado — alvo nunca recebe gradiente);
|
| 10 |
+
//! - regularizador variância-covariância (VICReg-lite) anti-colapso:
|
| 11 |
+
//! `V(z) = Σ_j max(0, γ − std(z_j))²` + fora-diagonal da covariância.
|
| 12 |
+
//! - perda: `L = Σ_b ‖P(z_ctx, pos_b) − sg(ẑ_b)‖²/B + λ_vc·V`.
|
| 13 |
+
//!
|
| 14 |
+
//! **Prova J-1 (impossibilidade de colapso, em r-20):** se todas as
|
| 15 |
+
//! incorporações colapsam para constante, `std(z_j) = 0 ∀j` ⇒ `V = d·γ² > 0`;
|
| 16 |
+
//! logo o colapso total NÃO é minimizador com `λ_vc > 0`.
|
| 17 |
+
//!
|
| 18 |
+
//! **Prova J-2 (EMA = stop-grad + ponto fixo):** `∂L/∂ξ = 0` (detach); se
|
| 19 |
+
//! `θ_t → θ*` então `ξ_t → θ*` (recursão linear contratante com `m < 1`).
|
| 20 |
+
//!
|
| 21 |
+
//! **Prova J-3 (decomposição de informação):** com `P` suficientemente
|
| 22 |
+
//! rico, `min_P L = Σ_b Var(ẑ_b | z_ctx)` — a perda retém só o componentes
|
| 23 |
+
//! previsíveis do alvo dado o contexto.
|
| 24 |
+
//!
|
| 25 |
+
//! **Variantes** (esquema de máscara + adaptador de modalidade):
|
| 26 |
+
//! Image-JEPA (blocos 2D) · V-JEPA 2 (tubos 3D + cond. de ação) · MC-JEPA
|
| 27 |
+
//! (cruz-modal, dois codificadores) · Audio-JEPA (bandas de espectrograma) ·
|
| 28 |
+
//! Point-JEPA (grupos FPS) · 3D-JEPA (voxels) · Brain-JEPA (canal×tempo) ·
|
| 29 |
+
//! T-JEPA (série temporal canal-independente) · LeJEPA (edição-condicionado) ·
|
| 30 |
+
//! BJEPA (bidirecional com consistência de ciclo).
|
| 31 |
+
//!
|
| 32 |
+
//! **SonicMoE** — MoE com roteamento top-k, capacidade por especialista
|
| 33 |
+
//! (tile-aware) e perda auxiliar de balanceamento (argumento Switch:
|
| 34 |
+
//! `L_aux = E·Σ f_e·p_e ≥ 1` com igualdade sse uniforme).
|
| 35 |
+
//!
|
| 36 |
+
//! **ReplaySSM** — SSM de porta diagonal (`h_t = σ(a)⊙h_{t−1} +
|
| 37 |
+
//! (1−σ(a))⊙(Bx_t)`, estabilidade herdada do Teo S-2') + replay de experiências
|
| 38 |
+
//! para aprendizado continuado (interpolação de gradientes limita o
|
| 39 |
+
//! esquecimento — Teo R-1).
|
| 40 |
+
|
| 41 |
+
use candle_core::{Device, Result, Tensor, Var};
|
| 42 |
+
use crate::camadas::Linear;
|
| 43 |
+
use crate::rng::RngDeterministico;
|
| 44 |
+
|
| 45 |
+
// ---- divisão SRP v0.10.0 (r-35 §2): submódulos + re-exports ----
|
| 46 |
+
#[cfg(test)]
|
| 47 |
+
mod testes;
|
| 48 |
+
mod mascaras;
|
| 49 |
+
mod nucleo;
|
| 50 |
+
mod patches;
|
| 51 |
+
mod replay_ssm;
|
| 52 |
+
mod sonic_moe;
|
| 53 |
+
mod variantes;
|
| 54 |
+
|
| 55 |
+
pub use mascaras::*;
|
| 56 |
+
pub use nucleo::*;
|
| 57 |
+
pub use patches::*;
|
| 58 |
+
pub use replay_ssm::*;
|
| 59 |
+
pub use sonic_moe::*;
|
| 60 |
+
pub use variantes::*;
|
| 61 |
+
|
src/jepa/nucleo.rs
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `jepa::nucleo` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
// ---------------------------------------------------------------------------
|
| 8 |
+
// Parte 1 — núcleo
|
| 9 |
+
// ---------------------------------------------------------------------------
|
| 10 |
+
|
| 11 |
+
/// MLP 2 camadas com SiLU (d → h → d_saida).
|
| 12 |
+
pub struct MlpJepa {
|
| 13 |
+
pub l1: Linear,
|
| 14 |
+
pub l2: Linear,
|
| 15 |
+
pub d_entrada: usize,
|
| 16 |
+
pub d_saida: usize,
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
impl MlpJepa {
|
| 20 |
+
pub fn novo(d_entrada: usize, d_oculta: usize, d_saida: usize, dev: &Device) -> Result<Self> {
|
| 21 |
+
let s1 = (1.0 / (d_entrada as f64).sqrt()) * 0.9;
|
| 22 |
+
let s2 = (1.0 / (d_oculta as f64).sqrt()) * 0.9;
|
| 23 |
+
Ok(Self {
|
| 24 |
+
l1: Linear::sem_vies(d_entrada, d_oculta, s1, dev)?,
|
| 25 |
+
l2: Linear::sem_vies(d_oculta, d_saida, s2, dev)?,
|
| 26 |
+
d_entrada,
|
| 27 |
+
d_saida,
|
| 28 |
+
})
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
pub fn fwd(&self, x: &Tensor) -> Result<Tensor> {
|
| 32 |
+
let h = candle_nn::ops::silu(&self.l1.fwd(x)?)?;
|
| 33 |
+
self.l2.fwd(&h)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
pub fn vars(&self) -> Vec<Var> {
|
| 37 |
+
[self.l1.vars(), self.l2.vars()].concat()
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/// EMA: `self ← m·self + (1−m)·outro` (usado no codificador-alvo).
|
| 41 |
+
pub fn ema_de(&mut self, outro: &MlpJepa, m: f64) -> Result<()> {
|
| 42 |
+
for (a, b) in self.vars().iter().zip(outro.vars().iter()) {
|
| 43 |
+
let novo = (a.as_tensor().affine(m, 0.0)? + b.as_tensor().affine(1.0 - m, 0.0)?)?;
|
| 44 |
+
a.set(&novo)?;
|
| 45 |
+
}
|
| 46 |
+
Ok(())
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/// Núcleo JEPA: codificador + preditor + alvo EMA + regularizador VC.
|
| 51 |
+
pub struct NucleoJepa {
|
| 52 |
+
pub encoder: MlpJepa,
|
| 53 |
+
pub preditor: MlpJepa,
|
| 54 |
+
pub alvo: MlpJepa,
|
| 55 |
+
/// tabela de posições (blocos) — entrada extra do preditor.
|
| 56 |
+
pub pos: Var,
|
| 57 |
+
pub d_emb: usize,
|
| 58 |
+
pub d_cond: usize,
|
| 59 |
+
pub m: f64,
|
| 60 |
+
pub lambda_vc: f64,
|
| 61 |
+
pub gamma_vc: f64,
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
impl NucleoJepa {
|
| 65 |
+
pub fn novo(
|
| 66 |
+
d_patch: usize,
|
| 67 |
+
d_emb: usize,
|
| 68 |
+
d_cond: usize,
|
| 69 |
+
n_pos: usize,
|
| 70 |
+
m: f64,
|
| 71 |
+
lambda_vc: f64,
|
| 72 |
+
gamma_vc: f64,
|
| 73 |
+
dev: &Device,
|
| 74 |
+
) -> Result<Self> {
|
| 75 |
+
let encoder = MlpJepa::novo(d_patch, 2 * d_emb, d_emb, dev)?;
|
| 76 |
+
let preditor = MlpJepa::novo(d_emb + d_cond + d_emb, 2 * d_emb, d_emb, dev)?;
|
| 77 |
+
// alvo nasce IGUAL ao encoder (ponto de partida do EMA)
|
| 78 |
+
let mut alvo = MlpJepa::novo(d_patch, 2 * d_emb, d_emb, dev)?;
|
| 79 |
+
for (va, vb) in alvo.vars().iter().zip(encoder.vars().iter()) {
|
| 80 |
+
va.set(vb.as_tensor())?;
|
| 81 |
+
}
|
| 82 |
+
let pos_t = Tensor::randn(0f32, 0.02, (n_pos.max(1), d_emb), dev)?;
|
| 83 |
+
Ok(Self {
|
| 84 |
+
encoder,
|
| 85 |
+
preditor,
|
| 86 |
+
alvo,
|
| 87 |
+
pos: Var::from_tensor(&pos_t)?,
|
| 88 |
+
d_emb,
|
| 89 |
+
d_cond,
|
| 90 |
+
m,
|
| 91 |
+
lambda_vc,
|
| 92 |
+
gamma_vc,
|
| 93 |
+
})
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
pub fn vars(&self) -> Vec<Var> {
|
| 97 |
+
let mut v = self.encoder.vars();
|
| 98 |
+
v.extend(self.preditor.vars());
|
| 99 |
+
v.push(self.pos.clone());
|
| 100 |
+
v // NOTA: alvo NÃO entra — é buffer EMA
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
pub fn ema_passo(&mut self) -> Result<()> {
|
| 104 |
+
self.alvo.ema_de(&self.encoder, self.m)
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/// Codifica um conjunto de patches (n, d_patch) → (n, d_emb).
|
| 108 |
+
pub fn codificar(&self, patches: &Tensor) -> Result<Tensor> {
|
| 109 |
+
self.encoder.fwd(patches)
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
/// Codifica com o alvo EMA — SEM gradiente (detach por construção: as
|
| 113 |
+
/// vars do alvo não estão no otimizador e o tensor é desconectado).
|
| 114 |
+
pub fn alvo_codificar(&self, patches: &Tensor) -> Result<Tensor> {
|
| 115 |
+
Ok(self.alvo.fwd(patches)?.detach())
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/// Prediz o embedding do bloco `i_pos` a partir do contexto `z_ctx`
|
| 119 |
+
/// (d_emb) e da condição `cond` (d_cond; zeros se None).
|
| 120 |
+
pub fn predizer(&self, z_ctx: &Tensor, i_pos: usize, cond: Option<&Tensor>) -> Result<Tensor> {
|
| 121 |
+
// z_ctx: (b, d_emb) ou (d_emb,)
|
| 122 |
+
let b = z_ctx.dim(0).unwrap_or(1);
|
| 123 |
+
let dev = z_ctx.device();
|
| 124 |
+
let pos = self.pos.as_tensor().get(i_pos.min(self.pos.dim(0)?.saturating_sub(1)))?;
|
| 125 |
+
let pos_b = pos.broadcast_as((b, self.d_emb))?;
|
| 126 |
+
let cond_b = match cond {
|
| 127 |
+
Some(c) => {
|
| 128 |
+
if c.dims().len() == 1 && b > 1 {
|
| 129 |
+
c.broadcast_as((b, self.d_cond))?
|
| 130 |
+
} else {
|
| 131 |
+
c.clone()
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
None => Tensor::zeros((b, self.d_cond), candle_core::DType::F32, dev)?,
|
| 135 |
+
};
|
| 136 |
+
let entrada = Tensor::cat(&[z_ctx, &pos_b, &cond_b], candle_core::D::Minus1)?;
|
| 137 |
+
self.preditor.fwd(&entrada)
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/// Regularizador variância-covariância do lote de incorporações z (n, d).
|
| 141 |
+
///
|
| 142 |
+
/// **J-1:** no colapso total `z_i = c ∀i` ⇒ `std_j = 0` ⇒ `V = d·γ²`.
|
| 143 |
+
pub fn perda_vc(&self, z: &Tensor) -> Result<Tensor> {
|
| 144 |
+
let n = z.dim(0)? as f64;
|
| 145 |
+
let d = self.d_emb;
|
| 146 |
+
let media = z.mean_keepdim(0)?; // (1, d)
|
| 147 |
+
let centr = z.broadcast_sub(&media)?;
|
| 148 |
+
let var = (centr.sqr()?.sum(0)? / n)?; // (d,)
|
| 149 |
+
let std = var.sqrt()?;
|
| 150 |
+
// dobradiça: max(0, γ − std)²
|
| 151 |
+
let gamma_t =
|
| 152 |
+
Tensor::new(self.gamma_vc as f32, z.device())?.broadcast_as(std.dims())?;
|
| 153 |
+
let gap = gamma_t.broadcast_sub(&std)?.clamp(0.0, f64::INFINITY)?;
|
| 154 |
+
let v = gap.sqr()?.sum_all()?;
|
| 155 |
+
// covariância fora-diagonal: C = centrᵀ·centr/n; perda = ‖C − diag‖²_F/(2d)
|
| 156 |
+
let cov = (centr.transpose(0, 1)?.matmul(¢r)? / n)?; // (d, d)
|
| 157 |
+
let mask_diag = {
|
| 158 |
+
let mut m = vec![0f32; d * d];
|
| 159 |
+
for i in 0..d {
|
| 160 |
+
m[i * d + i] = 1.0;
|
| 161 |
+
}
|
| 162 |
+
Tensor::from_vec(m, (d, d), z.device())?
|
| 163 |
+
};
|
| 164 |
+
let fora = ((&cov * &mask_diag)?.sqr()?.sum_all()? / (2.0 * d.max(1) as f64))?;
|
| 165 |
+
let v_norm = (v / d.max(1) as f64)?;
|
| 166 |
+
Ok((v_norm + fora)?)
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/// Perda JEPA padrão para UM lote: contexto (número variável de patches),
|
| 170 |
+
/// lista de blocos-alvo (posições), condição opcional.
|
| 171 |
+
///
|
| 172 |
+
/// `z_ctx` = incorporação agregada do contexto (b, d_emb);
|
| 173 |
+
/// `alvos`: para cada elemento do lote, Vec<(i_pos, patches_alvo)>.
|
| 174 |
+
pub fn perda_lote(
|
| 175 |
+
&self,
|
| 176 |
+
z_ctx: &Tensor,
|
| 177 |
+
alvos: &[Vec<(usize, Tensor)>],
|
| 178 |
+
cond: Option<&Tensor>,
|
| 179 |
+
z_ctx_todos: &Tensor,
|
| 180 |
+
) -> Result<(Tensor, f64, f64)> {
|
| 181 |
+
let b = z_ctx.dim(0)?;
|
| 182 |
+
let dev = z_ctx.device();
|
| 183 |
+
let mut perdas: Vec<Tensor> = Vec::new();
|
| 184 |
+
let mut n_alvos = 0usize;
|
| 185 |
+
for (i, blocos) in alvos.iter().enumerate() {
|
| 186 |
+
for (i_pos, patches_alvo) in blocos {
|
| 187 |
+
// ẑ_b = média das incorporações-alvo dos patches do bloco
|
| 188 |
+
let zt = self.alvo_codificar(patches_alvo)?; // (m, d_emb)
|
| 189 |
+
let zt_m = zt.mean(0)?; // (d_emb,)
|
| 190 |
+
let zt_b = zt_m.broadcast_as((1, self.d_emb))?;
|
| 191 |
+
let zc = z_ctx.get(i)?.broadcast_as((1, self.d_emb))?;
|
| 192 |
+
let c_i = cond.map(|c| c.get(i)?.broadcast_as((1, self.d_cond))).transpose()?;
|
| 193 |
+
let pred = self.predizer(&zc, *i_pos, c_i.as_ref())?;
|
| 194 |
+
let dif = (&pred - &zt_b)?;
|
| 195 |
+
let l = (dif.sqr()?.sum_all()? / self.d_emb as f64)?;
|
| 196 |
+
perdas.push(l);
|
| 197 |
+
n_alvos += 1;
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
let l_pred = if perdas.is_empty() {
|
| 201 |
+
Tensor::zeros((), candle_core::DType::F32, &dev)?
|
| 202 |
+
} else {
|
| 203 |
+
(Tensor::stack(&perdas, 0)?.sum_all()? / n_alvos.max(1) as f64)?
|
| 204 |
+
};
|
| 205 |
+
let l_vc = self.perda_vc(z_ctx_todos)?;
|
| 206 |
+
let total = (l_pred.affine(1.0, 0.0)? + l_vc.affine(self.lambda_vc, 0.0)?)?;
|
| 207 |
+
let lp = l_pred.to_scalar::<f32>()? as f64;
|
| 208 |
+
let lv = l_vc.to_scalar::<f32>()? as f64;
|
| 209 |
+
Ok((total, lp, lv))
|
| 210 |
+
}
|
| 211 |
+
}
|
src/jepa/patches.rs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `jepa::patches` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
// ---------------------------------------------------------------------------
|
| 8 |
+
// Parte 3 — adaptadores de modalidade
|
| 9 |
+
// ---------------------------------------------------------------------------
|
| 10 |
+
|
| 11 |
+
/// (C, H, W) → patches (H/ph · W/pw, C·ph·pw) — Image-JEPA.
|
| 12 |
+
pub fn em_patches_2d(img: &Tensor, ph: usize, pw: usize) -> Result<Tensor> {
|
| 13 |
+
let (c, h, w) = (img.dim(0)?, img.dim(1)?, img.dim(2)?);
|
| 14 |
+
let dev = img.device();
|
| 15 |
+
let mut linhas: Vec<Tensor> = Vec::new();
|
| 16 |
+
for y in (0..h).step_by(ph) {
|
| 17 |
+
for x in (0..w).step_by(pw) {
|
| 18 |
+
let hh = ph.min(h - y);
|
| 19 |
+
let ww = pw.min(w - x);
|
| 20 |
+
let patch = img.narrow(0, 0, c)?.narrow(1, y, hh)?.narrow(2, x, ww)?;
|
| 21 |
+
// padding para (C, ph, pw) se borda
|
| 22 |
+
let patch = if hh < ph || ww < pw {
|
| 23 |
+
let mut buf = patch.flatten_all()?.to_vec1::<f32>()?;
|
| 24 |
+
let falta = c * ph * pw - buf.len();
|
| 25 |
+
buf.extend(std::iter::repeat(0f32).take(falta));
|
| 26 |
+
Tensor::from_vec(buf, (c * ph * pw,), &dev)?
|
| 27 |
+
} else {
|
| 28 |
+
patch.flatten_all()?
|
| 29 |
+
};
|
| 30 |
+
linhas.push(patch);
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
Ok(Tensor::stack(&linhas, 0)?)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/// (T, H, W) → tubos (T/pt · H/ph · W/pw, pt·ph·pw) — V-JEPA 2.
|
| 37 |
+
pub fn em_tubos_3d(vid: &Tensor, pt: usize, ph: usize, pw: usize) -> Result<Tensor> {
|
| 38 |
+
let (t, h, w) = (vid.dim(0)?, vid.dim(1)?, vid.dim(2)?);
|
| 39 |
+
let dev = vid.device();
|
| 40 |
+
let mut linhas: Vec<Tensor> = Vec::new();
|
| 41 |
+
for tt in (0..t).step_by(pt) {
|
| 42 |
+
let tl = pt.min(t - tt);
|
| 43 |
+
for y in (0..h).step_by(ph) {
|
| 44 |
+
for x in (0..w).step_by(pw) {
|
| 45 |
+
let hh = ph.min(h - y);
|
| 46 |
+
let ww = pw.min(w - x);
|
| 47 |
+
let tubo = vid
|
| 48 |
+
.narrow(0, tt, tl)?
|
| 49 |
+
.narrow(1, y, hh)?
|
| 50 |
+
.narrow(2, x, ww)?
|
| 51 |
+
.flatten_all()?;
|
| 52 |
+
let mut buf = tubo.to_vec1::<f32>()?;
|
| 53 |
+
let falta = pt * ph * pw - buf.len();
|
| 54 |
+
buf.extend(std::iter::repeat(0f32).take(falta));
|
| 55 |
+
linhas.push(Tensor::from_vec(buf, (pt * ph * pw,), &dev)?);
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
Ok(Tensor::stack(&linhas, 0)?)
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/// (F, τ) → patches (F/pf · τ/pt, pf·pt) — Audio-JEPA.
|
| 63 |
+
pub fn em_patches_espectrograma(spec: &Tensor, pf: usize, pt: usize) -> Result<Tensor> {
|
| 64 |
+
let (f, tau) = (spec.dim(0)?, spec.dim(1)?);
|
| 65 |
+
let dev = spec.device();
|
| 66 |
+
let mut linhas: Vec<Tensor> = Vec::new();
|
| 67 |
+
for i in (0..f).step_by(pf) {
|
| 68 |
+
for j in (0..tau).step_by(pt) {
|
| 69 |
+
let ff = pf.min(f - i);
|
| 70 |
+
let tt = pt.min(tau - j);
|
| 71 |
+
let p = spec.narrow(0, i, ff)?.narrow(1, j, tt)?.flatten_all()?;
|
| 72 |
+
let mut buf = p.to_vec1::<f32>()?;
|
| 73 |
+
let falta = pf * pt - buf.len();
|
| 74 |
+
buf.extend(std::iter::repeat(0f32).take(falta));
|
| 75 |
+
linhas.push(Tensor::from_vec(buf, (pf * pt,), &dev)?);
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
Ok(Tensor::stack(&linhas, 0)?)
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/// (C, T) → patches canal-independentes (C · T/pt, pt) — T-JEPA/Brain-JEPA.
|
| 82 |
+
pub fn em_patches_serie(ts: &Tensor, pt: usize) -> Result<Tensor> {
|
| 83 |
+
let (c, t) = (ts.dim(0)?, ts.dim(1)?);
|
| 84 |
+
let dev = ts.device();
|
| 85 |
+
let mut linhas: Vec<Tensor> = Vec::new();
|
| 86 |
+
for ch in 0..c {
|
| 87 |
+
for j in (0..t).step_by(pt) {
|
| 88 |
+
let tt = pt.min(t - j);
|
| 89 |
+
let p = ts.narrow(0, ch, 1)?.narrow(1, j, tt)?.flatten_all()?;
|
| 90 |
+
let mut buf = p.to_vec1::<f32>()?;
|
| 91 |
+
let falta = pt - buf.len();
|
| 92 |
+
buf.extend(std::iter::repeat(0f32).take(falta));
|
| 93 |
+
linhas.push(Tensor::from_vec(buf, (pt,), &dev)?);
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
Ok(Tensor::stack(&linhas, 0)?)
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/// (X, Y, Z) → voxels (X/px · Y/py · Z/pz, px·py·pz) — 3D-JEPA.
|
| 100 |
+
pub fn em_voxels(vol: &Tensor, px: usize, py: usize, pz: usize) -> Result<Tensor> {
|
| 101 |
+
let (x, y, z) = (vol.dim(0)?, vol.dim(1)?, vol.dim(2)?);
|
| 102 |
+
let dev = vol.device();
|
| 103 |
+
let mut linhas: Vec<Tensor> = Vec::new();
|
| 104 |
+
for i in (0..x).step_by(px) {
|
| 105 |
+
for j in (0..y).step_by(py) {
|
| 106 |
+
for k in (0..z).step_by(pz) {
|
| 107 |
+
let lx = px.min(x - i);
|
| 108 |
+
let ly = py.min(y - j);
|
| 109 |
+
let lz = pz.min(z - k);
|
| 110 |
+
let p = vol.narrow(0, i, lx)?.narrow(1, j, ly)?.narrow(2, k, lz)?.flatten_all()?;
|
| 111 |
+
let mut buf = p.to_vec1::<f32>()?;
|
| 112 |
+
let falta = px * py * pz - buf.len();
|
| 113 |
+
buf.extend(std::iter::repeat(0f32).take(falta));
|
| 114 |
+
linhas.push(Tensor::from_vec(buf, (px * py * pz,), &dev)?);
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
Ok(Tensor::stack(&linhas, 0)?)
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
/// Nuvem de pontos (n, d) → grupos FPS (k, m·d): k centros por FPS, cada
|
| 122 |
+
/// grupo = os m vizinhos mais próximos do centro (concatenados) — Point-JEPA.
|
| 123 |
+
pub fn em_grupos_pontos(pc: &Tensor, k: usize, m: usize, rng: &mut RngDeterministico) -> Result<Tensor> {
|
| 124 |
+
let n = pc.dim(0)?;
|
| 125 |
+
let d = pc.dim(1)?;
|
| 126 |
+
let coords = pc.to_vec2::<f32>()?;
|
| 127 |
+
let coords_f: Vec<Vec<f64>> = coords.iter().map(|r| r.iter().map(|&v| v as f64).collect()).collect();
|
| 128 |
+
let centros = pontos_mais_distantes(&coords_f, k, rng);
|
| 129 |
+
let dev = pc.device();
|
| 130 |
+
let mut linhas: Vec<Tensor> = Vec::new();
|
| 131 |
+
for &ci in ¢ros {
|
| 132 |
+
// m vizinhos mais próximos do centro ci (incluindo ele próprio)
|
| 133 |
+
let mut ord: Vec<usize> = (0..n).collect();
|
| 134 |
+
ord.sort_by(|&a, &b| {
|
| 135 |
+
let da = distancia_eucl(&coords_f[a], &coords_f[ci]);
|
| 136 |
+
let db = distancia_eucl(&coords_f[b], &coords_f[ci]);
|
| 137 |
+
da.partial_cmp(&db).unwrap()
|
| 138 |
+
});
|
| 139 |
+
let mut buf: Vec<f32> = Vec::with_capacity(m * d);
|
| 140 |
+
for &idx in ord.iter().take(m) {
|
| 141 |
+
for j in 0..d {
|
| 142 |
+
buf.push(coords[idx][j]);
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
let falta = m * d - buf.len();
|
| 146 |
+
buf.extend(std::iter::repeat(0f32).take(falta));
|
| 147 |
+
linhas.push(Tensor::from_vec(buf, (m * d,), &dev)?);
|
| 148 |
+
}
|
| 149 |
+
Ok(Tensor::stack(&linhas, 0)?)
|
| 150 |
+
}
|
src/jepa/replay_ssm.rs
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `jepa::replay_ssm` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
// ---------------------------------------------------------------------------
|
| 8 |
+
// Parte 6 — ReplaySSM
|
| 9 |
+
// ---------------------------------------------------------------------------
|
| 10 |
+
|
| 11 |
+
/// SSM de porta diagonal com replay para aprendizado continuado.
|
| 12 |
+
///
|
| 13 |
+
/// Recorrência: `h_t = σ(a) ⊙ h_{t−1} + (1 − σ(a)) ⊙ (B x_t)`.
|
| 14 |
+
///
|
| 15 |
+
/// **Prova R-1 (estabilidade):** `|σ(a)| < 1` ⇒ perturbações de `h_0`
|
| 16 |
+
/// decaem geometricamente (mesma estrutura do Teo S-2' do SSD-Δ).
|
| 17 |
+
///
|
| 18 |
+
/// **Prova R-2 (limite de esquecimento):** o passo de treino mistura
|
| 19 |
+
/// gradientes `(1−α)·∇L_novo + α·∇L_replay`; para `L_antiga` μ-fortemente
|
| 20 |
+
/// convexa com gradientes novos limitados por `G`, o afastamento do ótimo
|
| 21 |
+
/// antigo por passo é `≤ (1−α)·η·G` (sem replay: `≤ η·G`) — o replay
|
| 22 |
+
/// contrai o drift por um fator `1/(1−α)` no regime linear.
|
| 23 |
+
pub struct ReplaySsm {
|
| 24 |
+
pub a: Var, // (d,) — porta (sigmoid no uso)
|
| 25 |
+
pub b: Var, // (d_in, d) — projeção de entrada
|
| 26 |
+
pub d: usize,
|
| 27 |
+
pub alpha_replay: f64,
|
| 28 |
+
pub buffer: Vec<Tensor>,
|
| 29 |
+
pub capacidade_buffer: usize,
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
impl ReplaySsm {
|
| 33 |
+
pub fn novo(d_in: usize, d: usize, alpha_replay: f64, capacidade_buffer: usize, dev: &Device) -> Result<Self> {
|
| 34 |
+
let a_t = Tensor::randn(0f32, 0.5, (d,), dev)?;
|
| 35 |
+
let b_t = Tensor::randn(0f32, 0.1, (d_in, d), dev)?;
|
| 36 |
+
Ok(Self {
|
| 37 |
+
a: Var::from_tensor(&a_t)?,
|
| 38 |
+
b: Var::from_tensor(&b_t)?,
|
| 39 |
+
d,
|
| 40 |
+
alpha_replay,
|
| 41 |
+
buffer: Vec::new(),
|
| 42 |
+
capacidade_buffer,
|
| 43 |
+
})
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
pub fn vars(&self) -> Vec<Var> {
|
| 47 |
+
vec![self.a.clone(), self.b.clone()]
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/// Scan: x (T, d_in) → h (T, d). Diferenciável (candle autograd).
|
| 51 |
+
pub fn scan(&self, x: &Tensor) -> Result<Tensor> {
|
| 52 |
+
let t = x.dim(0)?;
|
| 53 |
+
let dev = x.device();
|
| 54 |
+
let porta = candle_nn::ops::sigmoid(&self.a.as_tensor().broadcast_as((1, self.d))?)?; // (1, d)
|
| 55 |
+
let um_menos = porta.affine(-1.0, 1.0)?; // 1 − σ(a)
|
| 56 |
+
let entrada = x.matmul(&self.b.as_tensor())?; // (T, d)
|
| 57 |
+
let mut h = Tensor::zeros((1, self.d), candle_core::DType::F32, &dev)?;
|
| 58 |
+
let mut saidas: Vec<Tensor> = Vec::with_capacity(t);
|
| 59 |
+
for i in 0..t {
|
| 60 |
+
let xt = entrada.narrow(0, i, 1)?;
|
| 61 |
+
h = ((&porta * &h)? + (&um_menos * &xt)?)?;
|
| 62 |
+
saidas.push(h.clone());
|
| 63 |
+
}
|
| 64 |
+
Ok(Tensor::stack(&saidas, 0)?)
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/// Empurra uma janela de treino no buffer (FIFO).
|
| 68 |
+
pub fn guardar(&mut self, x: &Tensor) {
|
| 69 |
+
if self.buffer.len() >= self.capacidade_buffer {
|
| 70 |
+
self.buffer.remove(0);
|
| 71 |
+
}
|
| 72 |
+
self.buffer.push(x.detach());
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
/// Um passo de treino no lote novo (com mistura de replay).
|
| 76 |
+
/// Devolve (perda_total, perda_novo, perda_replay).
|
| 77 |
+
pub fn passo_treino(
|
| 78 |
+
&mut self,
|
| 79 |
+
x: &Tensor,
|
| 80 |
+
alvo: &Tensor,
|
| 81 |
+
otimizador: &mut crate::treino::AdamWManual,
|
| 82 |
+
) -> Result<(f64, f64, f64)> {
|
| 83 |
+
// perda nova
|
| 84 |
+
let h = self.scan(x)?;
|
| 85 |
+
let dif = (&h - alvo)?;
|
| 86 |
+
let perda_novo = dif.sqr()?.mean_all()?;
|
| 87 |
+
// replay
|
| 88 |
+
let mut perda_total = perda_novo.clone();
|
| 89 |
+
let mut p_r = 0.0;
|
| 90 |
+
if !self.buffer.is_empty() {
|
| 91 |
+
let idx = self.buffer.len() - 1;
|
| 92 |
+
let antigo = self.buffer[idx].clone();
|
| 93 |
+
let hr = self.scan(&antigo)?;
|
| 94 |
+
// alvo do replay: manter a dinâmica aprendida estável (magnitudes
|
| 95 |
+
// finitas) — auto-supervisão de estabilidade
|
| 96 |
+
let dr = hr.sqr()?.mean_all()?;
|
| 97 |
+
perda_total = (perda_total.affine(1.0 - self.alpha_replay, 0.0)?
|
| 98 |
+
+ dr.affine(self.alpha_replay, 0.0)?)?;
|
| 99 |
+
p_r = dr.to_scalar::<f32>()? as f64;
|
| 100 |
+
}
|
| 101 |
+
let pn = perda_novo.to_scalar::<f32>()? as f64;
|
| 102 |
+
otimizador.passo(crate::treino::grads_achatados(&perda_total, &self.vars())?);
|
| 103 |
+
self.guardar(x);
|
| 104 |
+
let pt = perda_total.to_scalar::<f32>()? as f64;
|
| 105 |
+
Ok((pt, pn, p_r))
|
| 106 |
+
}
|
| 107 |
+
}
|
src/jepa/sonic_moe.rs
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `jepa::sonic_moe` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
// ---------------------------------------------------------------------------
|
| 8 |
+
// Parte 5 — SonicMoE (IO/tile-aware)
|
| 9 |
+
// ---------------------------------------------------------------------------
|
| 10 |
+
|
| 11 |
+
/// MoE top-k com capacidade por especialista e perda de balanceamento.
|
| 12 |
+
///
|
| 13 |
+
/// **Prova SM-1 (limite de capacidade):** o roteamento tile-aware mantém no
|
| 14 |
+
/// máximo `C` tokens por especialista por ladrilho — o drop é determinístico
|
| 15 |
+
/// (ordem de chegada) e o nº de tokens descartados é `Σ_e max(0, load_e − C)`.
|
| 16 |
+
///
|
| 17 |
+
/// **Prova SM-2 (balanceamento):** `L_aux = E·Σ_e f_e·p_e`; com roteamento
|
| 18 |
+
/// auto-consistente (`f = p`), `L_aux = E·Σ p_e² ≥ 1` com igualdade sse
|
| 19 |
+
/// `p_e = 1/E` (Cauchy–Schchwarz) — o mínimo força o roteador uniforme.
|
| 20 |
+
pub struct SonicMoe {
|
| 21 |
+
pub roteador: Linear, // d → E
|
| 22 |
+
pub especialistas: Vec<MlpJepa>, // E experts d → d
|
| 23 |
+
pub n_top: usize,
|
| 24 |
+
pub capacidade: usize,
|
| 25 |
+
pub lambda_aux: f64,
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
pub struct SaidaMoe {
|
| 29 |
+
pub saida: Tensor,
|
| 30 |
+
pub perda_aux: Tensor,
|
| 31 |
+
pub carga: Vec<usize>, // tokens efetivamente processados por especialista
|
| 32 |
+
pub descartados: usize, // tokens com especialistas cortados pela capacidade
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
impl SonicMoe {
|
| 36 |
+
pub fn novo(d: usize, n_expertos: usize, n_top: usize, capacidade: usize, dev: &Device) -> Result<Self> {
|
| 37 |
+
let roteador = Linear::sem_vies(d, n_expertos, 0.1, dev)?;
|
| 38 |
+
let especialistas = (0..n_expertos)
|
| 39 |
+
.map(|_| MlpJepa::novo(d, 2 * d, d, dev))
|
| 40 |
+
.collect::<Result<Vec<_>>>()?;
|
| 41 |
+
Ok(Self { roteador, especialistas, n_top, capacidade, lambda_aux: 0.01 })
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
pub fn vars(&self) -> Vec<Var> {
|
| 45 |
+
let mut v = self.roteador.vars();
|
| 46 |
+
for e in &self.especialistas {
|
| 47 |
+
v.extend(e.vars());
|
| 48 |
+
}
|
| 49 |
+
v
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/// Forward tile-aware: (n, d) → (n, d).
|
| 53 |
+
pub fn fwd(&self, x: &Tensor) -> Result<SaidaMoe> {
|
| 54 |
+
let n = x.dim(0)?;
|
| 55 |
+
let d = x.dim(1)?;
|
| 56 |
+
let e = self.especialistas.len();
|
| 57 |
+
let dev = x.device();
|
| 58 |
+
let logits = self.roteador.fwd(x)?; // (n, E)
|
| 59 |
+
let probs = candle_nn::ops::softmax(&logits, candle_core::D::Minus1)?;
|
| 60 |
+
|
| 61 |
+
// top-k por token (não diferenciável — pesos do top-k são dif. via probs)
|
| 62 |
+
let probs_v = probs.to_vec2::<f32>()?;
|
| 63 |
+
let mut carga = vec![0usize; e];
|
| 64 |
+
let mut descartados = 0usize;
|
| 65 |
+
// acumulador de saída (n, d) — construído por soma de contribuições
|
| 66 |
+
let mut saida = Tensor::zeros((n, d), candle_core::DType::F32, &dev)?;
|
| 67 |
+
let mut peso_total = Tensor::zeros((n, 1), candle_core::DType::F32, &dev)?;
|
| 68 |
+
// por especialista (tile): tokens cujo top-k contém e e com carga < C
|
| 69 |
+
for ei in 0..e {
|
| 70 |
+
// candidatos: tokens ordenados por prob do especialista ei
|
| 71 |
+
let mut cand: Vec<(usize, f32)> = (0..n)
|
| 72 |
+
.map(|i| (i, probs_v[i][ei]))
|
| 73 |
+
.collect();
|
| 74 |
+
cand.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
|
| 75 |
+
let mut escolhidos: Vec<usize> = Vec::new();
|
| 76 |
+
for (i, _) in cand {
|
| 77 |
+
if carga[ei] >= self.capacidade {
|
| 78 |
+
break;
|
| 79 |
+
}
|
| 80 |
+
// verifica se ei está no top-k do token i
|
| 81 |
+
let mut ordem: Vec<(f32, usize)> =
|
| 82 |
+
(0..e).map(|k| (probs_v[i][k], k)).collect();
|
| 83 |
+
ordem.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
|
| 84 |
+
let top: Vec<usize> = ordem.iter().rev().take(self.n_top).map(|(_, k)| *k).collect();
|
| 85 |
+
if top.contains(&ei) {
|
| 86 |
+
escolhidos.push(i);
|
| 87 |
+
carga[ei] += 1;
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
if escolhidos.is_empty() {
|
| 91 |
+
continue;
|
| 92 |
+
}
|
| 93 |
+
// tile do especialista: uma única passagem em lote
|
| 94 |
+
let idx: Vec<u32> = escolhidos.iter().map(|&i| i as u32).collect();
|
| 95 |
+
let n_e = idx.len();
|
| 96 |
+
let idx_t = Tensor::from_vec(idx, (n_e,), &dev)?;
|
| 97 |
+
let tile = x.index_select(&idx_t, 0)?;
|
| 98 |
+
let y = self.especialistas[ei].fwd(&tile)?; // (m, d)
|
| 99 |
+
// pesos: prob do especialista ei para cada token escolhido
|
| 100 |
+
let w: Vec<f32> = escolhidos.iter().map(|&i| probs_v[i][ei]).collect();
|
| 101 |
+
let n_w = escolhidos.len();
|
| 102 |
+
let w_t = Tensor::from_vec(w.clone(), (n_w, 1), &dev)?;
|
| 103 |
+
let contrib = y.broadcast_mul(&w_t)?;
|
| 104 |
+
// scatter-add: acumula nas posições
|
| 105 |
+
let mut buf = saida.to_vec2::<f32>()?;
|
| 106 |
+
let contrib_v = contrib.to_vec2::<f32>()?;
|
| 107 |
+
for (k, &i) in escolhidos.iter().enumerate() {
|
| 108 |
+
for j in 0..d {
|
| 109 |
+
buf[i][j] += contrib_v[k][j];
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
saida = Tensor::from_vec(
|
| 113 |
+
buf.into_iter().flatten().collect::<Vec<f32>>(),
|
| 114 |
+
(n, d),
|
| 115 |
+
&dev,
|
| 116 |
+
)?;
|
| 117 |
+
let mut pt = peso_total.to_vec2::<f32>()?;
|
| 118 |
+
for (k, &i) in escolhidos.iter().enumerate() {
|
| 119 |
+
pt[i][0] += w[k];
|
| 120 |
+
}
|
| 121 |
+
peso_total = Tensor::from_vec(
|
| 122 |
+
pt.into_iter().flatten().collect::<Vec<f32>>(),
|
| 123 |
+
(n, 1),
|
| 124 |
+
&dev,
|
| 125 |
+
)?;
|
| 126 |
+
}
|
| 127 |
+
// tokens sem especialista (todos cortados) → descartados; saída 0
|
| 128 |
+
let pt_v = peso_total.to_vec2::<f32>()?;
|
| 129 |
+
for i in 0..n {
|
| 130 |
+
if pt_v[i][0] == 0.0 {
|
| 131 |
+
descartados += 1;
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
// renormaliza pelo peso efetivo (evita viés da capacidade)
|
| 135 |
+
let denom = peso_total.clamp(1e-6, f64::INFINITY)?;
|
| 136 |
+
let saida = saida.broadcast_div(&denom)?;
|
| 137 |
+
|
| 138 |
+
// perda auxiliar: E·Σ f_e·p_e (f = fração de tokens cujo top-1 é e)
|
| 139 |
+
let mut f = vec![0f32; e];
|
| 140 |
+
for i in 0..n {
|
| 141 |
+
let (mut melhor, mut mp) = (0usize, f32::NEG_INFINITY);
|
| 142 |
+
for k in 0..e {
|
| 143 |
+
if probs_v[i][k] > mp {
|
| 144 |
+
mp = probs_v[i][k];
|
| 145 |
+
melhor = k;
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
f[melhor] += 1.0 / n as f32;
|
| 149 |
+
}
|
| 150 |
+
let f_t = Tensor::from_vec(f, (e,), &dev)?;
|
| 151 |
+
let p_media = probs.mean(0)?; // (E,)
|
| 152 |
+
let perda_aux = ((&f_t * &p_media)?.sum_all()? * (e as f64))?;
|
| 153 |
+
Ok(SaidaMoe { saida, perda_aux, carga, descartados })
|
| 154 |
+
}
|
| 155 |
+
}
|
src/jepa/testes.rs
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `jepa::testes` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
#[cfg(test)]
|
| 8 |
+
mod testes {
|
| 9 |
+
use super::*;
|
| 10 |
+
|
| 11 |
+
fn tensor_2d(dados: &[Vec<f64>], dev: &Device) -> Tensor {
|
| 12 |
+
let flat: Vec<f32> = dados.iter().flat_map(|v| v.iter().map(|&x| x as f32)).collect();
|
| 13 |
+
Tensor::from_vec(flat, (dados.len(), dados[0].len()), dev).unwrap()
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
#[test]
|
| 17 |
+
fn teste_mascaras_validas() {
|
| 18 |
+
// fração respeitada ±, particionamento contexto/alvo não-vazio
|
| 19 |
+
let mut rng = RngDeterministico::novo(1);
|
| 20 |
+
let m2 = mascara_blocos_2d(8, 8, 4, 0.3, &mut rng);
|
| 21 |
+
assert_eq!(m2.len(), 64);
|
| 22 |
+
let frac = m2.iter().filter(|&&b| b).count() as f64 / 64.0;
|
| 23 |
+
assert!(frac >= 0.15 && frac <= 0.55, "frac 2d = {frac}");
|
| 24 |
+
assert!(m2.iter().any(|&b| !b), "contexto vazio");
|
| 25 |
+
|
| 26 |
+
let m3 = mascara_tubos_3d(4, 6, 6, 3, 0.3, &mut rng);
|
| 27 |
+
assert_eq!(m3.len(), 144);
|
| 28 |
+
let f3 = m3.iter().filter(|&&b| b).count() as f64 / 144.0;
|
| 29 |
+
assert!(f3 >= 0.15 && f3 <= 0.6, "frac 3d = {f3}");
|
| 30 |
+
|
| 31 |
+
let ms = mascara_segmentos(4, 32, 0.25, &mut rng);
|
| 32 |
+
assert_eq!(ms.len(), 128);
|
| 33 |
+
assert!(ms.iter().any(|&b| b) && ms.iter().any(|&b| !b));
|
| 34 |
+
|
| 35 |
+
let m1 = mascara_intervalos_1d(64, 0.3, &mut rng);
|
| 36 |
+
let f1 = m1.iter().filter(|&&b| b).count() as f64 / 64.0;
|
| 37 |
+
assert!(f1 >= 0.25 && f1 <= 0.45, "frac 1d = {f1}");
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
#[test]
|
| 41 |
+
fn teste_fps_cobre() {
|
| 42 |
+
let mut rng = RngDeterministico::novo(2);
|
| 43 |
+
let pontos: Vec<Vec<f64>> = (0..40)
|
| 44 |
+
.map(|i| {
|
| 45 |
+
let a = i as f64 * 0.1;
|
| 46 |
+
vec![a.cos() * 3.0, a.sin() * 3.0, (i % 7) as f64 * 0.3]
|
| 47 |
+
})
|
| 48 |
+
.collect();
|
| 49 |
+
let centros = pontos_mais_distantes(&pontos, 6, &mut rng);
|
| 50 |
+
assert_eq!(centros.len(), 6);
|
| 51 |
+
// centros distintos
|
| 52 |
+
let mut s = centros.clone();
|
| 53 |
+
s.sort();
|
| 54 |
+
s.dedup();
|
| 55 |
+
assert_eq!(s.len(), 6);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
#[test]
|
| 59 |
+
fn teste_j1_colapso_impossivel() {
|
| 60 |
+
// No colapso total (z constante), a perda VC = γ² > 0 — verificando a
|
| 61 |
+
// fórmula implementada com um lote constante.
|
| 62 |
+
let dev = Device::Cpu;
|
| 63 |
+
let nucleo = NucleoJepa::novo(4, 8, 0, 4, 0.99, 1.0, 1.0, &dev).unwrap();
|
| 64 |
+
let z = Tensor::from_vec(vec![0.5f32; 8 * 8], (8, 8), &dev).unwrap();
|
| 65 |
+
let lvc = nucleo.perda_vc(&z).unwrap();
|
| 66 |
+
let v = lvc.to_scalar::<f32>().unwrap() as f64;
|
| 67 |
+
// std=0 ⇒ dobradiça = γ = 1 ⇒ V/d = 1; cov = 0
|
| 68 |
+
assert!(v > 0.9, "perda VC no colapso = {v} (deveria ser ~1)");
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
#[test]
|
| 72 |
+
fn teste_j2_ema_ponto_fixo() {
|
| 73 |
+
// encoder congelado ⇒ após muitas passadas EMA, alvo = encoder
|
| 74 |
+
let dev = Device::Cpu;
|
| 75 |
+
let mut nucleo = NucleoJepa::novo(4, 8, 0, 4, 0.5, 0.1, 1.0, &dev).unwrap();
|
| 76 |
+
for _ in 0..40 {
|
| 77 |
+
nucleo.ema_passo().unwrap();
|
| 78 |
+
}
|
| 79 |
+
let a = nucleo.alvo.vars()[0].as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap();
|
| 80 |
+
let b = nucleo.encoder.vars()[0].as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap();
|
| 81 |
+
let dif: f32 = a.iter().zip(&b).map(|(x, y)| (x - y).abs()).fold(0.0, f32::max);
|
| 82 |
+
assert!(dif < 1e-5, "EMA não convergiu: dif máx = {dif}");
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
#[test]
|
| 86 |
+
fn teste_jepa_aprende_latente_previsivel() {
|
| 87 |
+
// J-3: com alvo previsível (função determinística do contexto), a
|
| 88 |
+
// perda pred must cair substancialmente e o std das incorporações
|
| 89 |
+
// NÃO colapsa (J-1).
|
| 90 |
+
let dev = Device::Cpu;
|
| 91 |
+
let mut rng = RngDeterministico::novo(3);
|
| 92 |
+
// patches = 64 vetores 8-D em 4 grupos (estrutura previsível)
|
| 93 |
+
let mut dados: Vec<Vec<f64>> = Vec::new();
|
| 94 |
+
for g in 0..4 {
|
| 95 |
+
let c = g as f64 * 2.0;
|
| 96 |
+
for _ in 0..16 {
|
| 97 |
+
dados.push((0..8).map(|k| c + rng.normal(0.0, 0.3) + (k as f64) * 0.05).collect());
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
let patches = tensor_2d(&dados, &dev);
|
| 101 |
+
let cfg = CfgJepa { d_emb: 16, epocas: 40, lr: 5e-3, ..Default::default() };
|
| 102 |
+
let rel = treinar_variante(
|
| 103 |
+
&patches,
|
| 104 |
+
&|n, r| mascara_intervalos_1d(n, 0.3, r),
|
| 105 |
+
&cfg,
|
| 106 |
+
VarianteJepa::TJepa,
|
| 107 |
+
None,
|
| 108 |
+
&dev,
|
| 109 |
+
)
|
| 110 |
+
.unwrap();
|
| 111 |
+
assert!(
|
| 112 |
+
rel.perda_final < rel.perda_inicial * 0.8,
|
| 113 |
+
"perda deveria cair: {} → {}",
|
| 114 |
+
rel.perda_inicial,
|
| 115 |
+
rel.perda_final
|
| 116 |
+
);
|
| 117 |
+
assert!(
|
| 118 |
+
rel.std_emb_final > 0.05,
|
| 119 |
+
"sem colapso: std_final = {}",
|
| 120 |
+
rel.std_emb_final
|
| 121 |
+
);
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
#[test]
|
| 125 |
+
fn teste_adaptadores_formas() {
|
| 126 |
+
let dev = Device::Cpu;
|
| 127 |
+
let img = Tensor::randn(0f32, 1.0, (3, 16, 16), &dev).unwrap();
|
| 128 |
+
let p = em_patches_2d(&img, 4, 4).unwrap();
|
| 129 |
+
assert_eq!(p.dims().to_vec(), vec![16, 48]);
|
| 130 |
+
|
| 131 |
+
let vid = Tensor::randn(0f32, 1.0, (4, 8, 8), &dev).unwrap();
|
| 132 |
+
let t3 = em_tubos_3d(&vid, 2, 4, 4).unwrap();
|
| 133 |
+
assert_eq!(t3.dims().to_vec(), vec![8, 32]);
|
| 134 |
+
|
| 135 |
+
let spec = Tensor::randn(0f32, 1.0, (16, 32), &dev).unwrap();
|
| 136 |
+
let ps = em_patches_espectrograma(&spec, 4, 8).unwrap();
|
| 137 |
+
assert_eq!(ps.dims().to_vec(), vec![16, 32]);
|
| 138 |
+
|
| 139 |
+
let ts = Tensor::randn(0f32, 1.0, (4, 32), &dev).unwrap();
|
| 140 |
+
let pt_ = em_patches_serie(&ts, 8).unwrap();
|
| 141 |
+
assert_eq!(pt_.dims().to_vec(), vec![16, 8]);
|
| 142 |
+
|
| 143 |
+
let vol = Tensor::randn(0f32, 1.0, (8, 8, 8), &dev).unwrap();
|
| 144 |
+
let vx = em_voxels(&vol, 4, 4, 4).unwrap();
|
| 145 |
+
assert_eq!(vx.dims().to_vec(), vec![8, 64]);
|
| 146 |
+
|
| 147 |
+
let pc = Tensor::randn(0f32, 1.0, (50, 3), &dev).unwrap();
|
| 148 |
+
let mut rng = RngDeterministico::novo(5);
|
| 149 |
+
let gr = em_grupos_pontos(&pc, 6, 4, &mut rng).unwrap();
|
| 150 |
+
assert_eq!(gr.dims().to_vec(), vec![6, 12]);
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
#[test]
|
| 154 |
+
fn teste_sonic_moe_capacidade_e_balanceamento() {
|
| 155 |
+
// SM-1: carga por especialista ≤ capacidade; SM-2: aux ≥ ~1 quando
|
| 156 |
+
// roteador é ~uniforme (início).
|
| 157 |
+
let dev = Device::Cpu;
|
| 158 |
+
let moe = SonicMoe::novo(8, 4, 2, 6, &dev).unwrap();
|
| 159 |
+
let x = Tensor::randn(0f32, 1.0, (20, 8), &dev).unwrap();
|
| 160 |
+
let s = moe.fwd(&x).unwrap();
|
| 161 |
+
for (e, c) in s.carga.iter().enumerate() {
|
| 162 |
+
assert!(*c <= 6, "especialista {e} com carga {c} > capacidade 6");
|
| 163 |
+
}
|
| 164 |
+
assert_eq!(s.saida.dims().to_vec(), vec![20, 8]);
|
| 165 |
+
let aux = s.perda_aux.to_scalar::<f32>().unwrap() as f64;
|
| 166 |
+
// com roteador quase uniforme, aux ≈ E·Σ (1/E)·(1/E) = 1
|
| 167 |
+
assert!(aux > 0.5 && aux < 3.0, "aux = {aux}");
|
| 168 |
+
// cada token processado por ≤ n_top especialistas
|
| 169 |
+
let total_carga: usize = s.carga.iter().sum();
|
| 170 |
+
assert!(total_carga <= 20 * 2, "carga total {total_carga} > n·topk");
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
#[test]
|
| 174 |
+
fn teste_replay_ssm_estabilidade_e_replay() {
|
| 175 |
+
// R-1: estados finitos e limitados; R-2: com replay α>0, após treinar
|
| 176 |
+
// a tarefa nova, a dinâmica antiga (magnitude do estado) permanece
|
| 177 |
+
// estável (não diverge).
|
| 178 |
+
let dev = Device::Cpu;
|
| 179 |
+
let mut ssm = ReplaySsm::novo(4, 6, 0.5, 8, &dev).unwrap();
|
| 180 |
+
let vars = ssm.vars();
|
| 181 |
+
let mut otim = crate::treino::AdamWManual::novo(vars, 1e-2, 0.0);
|
| 182 |
+
|
| 183 |
+
// tarefa "antiga": sinal constante 1.0
|
| 184 |
+
let x_antigo = Tensor::from_vec(vec![1.0f32; 16 * 4], (16, 4), &dev).unwrap();
|
| 185 |
+
for _ in 0..30 {
|
| 186 |
+
let h = ssm.scan(&x_antigo).unwrap();
|
| 187 |
+
let alvo = h.detach();
|
| 188 |
+
ssm.passo_treino(&x_antigo, &alvo, &mut otim).unwrap();
|
| 189 |
+
}
|
| 190 |
+
// estado após treino antigo: finito
|
| 191 |
+
let h1 = ssm.scan(&x_antigo).unwrap();
|
| 192 |
+
let m1 = h1.abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
|
| 193 |
+
assert!(m1.is_finite() && m1 < 50.0, "estado explodiu: {m1}");
|
| 194 |
+
|
| 195 |
+
// tarefa "nova": sinal −1.0 — treino com replay
|
| 196 |
+
let x_novo = Tensor::from_vec(vec![-1.0f32; 16 * 4], (16, 4), &dev).unwrap();
|
| 197 |
+
for _ in 0..30 {
|
| 198 |
+
let h = ssm.scan(&x_novo).unwrap();
|
| 199 |
+
let alvo = h.detach();
|
| 200 |
+
ssm.passo_treino(&x_novo, &alvo, &mut otim).unwrap();
|
| 201 |
+
}
|
| 202 |
+
// a dinâmica antiga continua estável (sem explosão)
|
| 203 |
+
let h2 = ssm.scan(&x_antigo).unwrap();
|
| 204 |
+
let m2 = h2.abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
|
| 205 |
+
assert!(m2.is_finite() && m2 < 50.0, "estado antigo explodiu: {m2}");
|
| 206 |
+
// portas dentro de (0,1)
|
| 207 |
+
let a = ssm.a.as_tensor().to_vec1::<f32>().unwrap();
|
| 208 |
+
for v in a {
|
| 209 |
+
assert!(v.is_finite());
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
}
|
src/{jepa.rs → jepa/variantes.rs}
RENAMED
|
@@ -1,563 +1,8 @@
|
|
| 1 |
-
//!
|
| 2 |
-
//!
|
| 3 |
-
//! **Núcleo (todas as variantes compartilham):**
|
| 4 |
-
//! - codificador `E_θ` (MLp SiLU), preditor `P_φ` (condicionado por posição/
|
| 5 |
-
//! ação/edição), codificador-alvo EMA `E_ξ` com `ξ ← m·ξ + (1−m)·θ`
|
| 6 |
-
//! (gradiente cortado — alvo nunca recebe gradiente);
|
| 7 |
-
//! - regularizador variância-covariância (VICReg-lite) anti-colapso:
|
| 8 |
-
//! `V(z) = Σ_j max(0, γ − std(z_j))²` + fora-diagonal da covariância.
|
| 9 |
-
//! - perda: `L = Σ_b ‖P(z_ctx, pos_b) − sg(ẑ_b)‖²/B + λ_vc·V`.
|
| 10 |
-
//!
|
| 11 |
-
//! **Prova J-1 (impossibilidade de colapso, em r-20):** se todas as
|
| 12 |
-
//! incorporações colapsam para constante, `std(z_j) = 0 ∀j` ⇒ `V = d·γ² > 0`;
|
| 13 |
-
//! logo o colapso total NÃO é minimizador com `λ_vc > 0`.
|
| 14 |
-
//!
|
| 15 |
-
//! **Prova J-2 (EMA = stop-grad + ponto fixo):** `∂L/∂ξ = 0` (detach); se
|
| 16 |
-
//! `θ_t → θ*` então `ξ_t → θ*` (recursão linear contratante com `m < 1`).
|
| 17 |
-
//!
|
| 18 |
-
//! **Prova J-3 (decomposição de informação):** com `P` suficientemente
|
| 19 |
-
//! rico, `min_P L = Σ_b Var(ẑ_b | z_ctx)` — a perda retém só o componentes
|
| 20 |
-
//! previsíveis do alvo dado o contexto.
|
| 21 |
-
//!
|
| 22 |
-
//! **Variantes** (esquema de máscara + adaptador de modalidade):
|
| 23 |
-
//! Image-JEPA (blocos 2D) · V-JEPA 2 (tubos 3D + cond. de ação) · MC-JEPA
|
| 24 |
-
//! (cruz-modal, dois codificadores) · Audio-JEPA (bandas de espectrograma) ·
|
| 25 |
-
//! Point-JEPA (grupos FPS) · 3D-JEPA (voxels) · Brain-JEPA (canal×tempo) ·
|
| 26 |
-
//! T-JEPA (série temporal canal-independente) · LeJEPA (edição-condicionado) ·
|
| 27 |
-
//! BJEPA (bidirecional com consistência de ciclo).
|
| 28 |
-
//!
|
| 29 |
-
//! **SonicMoE** — MoE com roteamento top-k, capacidade por especialista
|
| 30 |
-
//! (tile-aware) e perda auxiliar de balanceamento (argumento Switch:
|
| 31 |
-
//! `L_aux = E·Σ f_e·p_e ≥ 1` com igualdade sse uniforme).
|
| 32 |
-
//!
|
| 33 |
-
//! **ReplaySSM** — SSM de porta diagonal (`h_t = σ(a)⊙h_{t−1} +
|
| 34 |
-
//! (1−σ(a))⊙(Bx_t)`, estabilidade herdada do Teo S-2') + replay de experiências
|
| 35 |
-
//! para aprendizado continuado (interpolação de gradientes limita o
|
| 36 |
-
//! esquecimento — Teo R-1).
|
| 37 |
|
| 38 |
-
use
|
| 39 |
-
use crate::camadas::Linear;
|
| 40 |
-
use crate::rng::RngDeterministico;
|
| 41 |
-
use std::collections::HashMap;
|
| 42 |
|
| 43 |
-
// ---------------------------------------------------------------------------
|
| 44 |
-
// Parte 1 — núcleo
|
| 45 |
-
// ---------------------------------------------------------------------------
|
| 46 |
-
|
| 47 |
-
/// MLP 2 camadas com SiLU (d → h → d_saida).
|
| 48 |
-
pub struct MlpJepa {
|
| 49 |
-
pub l1: Linear,
|
| 50 |
-
pub l2: Linear,
|
| 51 |
-
pub d_entrada: usize,
|
| 52 |
-
pub d_saida: usize,
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
impl MlpJepa {
|
| 56 |
-
pub fn novo(d_entrada: usize, d_oculta: usize, d_saida: usize, dev: &Device) -> Result<Self> {
|
| 57 |
-
let s1 = (1.0 / (d_entrada as f64).sqrt()) * 0.9;
|
| 58 |
-
let s2 = (1.0 / (d_oculta as f64).sqrt()) * 0.9;
|
| 59 |
-
Ok(Self {
|
| 60 |
-
l1: Linear::sem_vies(d_entrada, d_oculta, s1, dev)?,
|
| 61 |
-
l2: Linear::sem_vies(d_oculta, d_saida, s2, dev)?,
|
| 62 |
-
d_entrada,
|
| 63 |
-
d_saida,
|
| 64 |
-
})
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
pub fn fwd(&self, x: &Tensor) -> Result<Tensor> {
|
| 68 |
-
let h = candle_nn::ops::silu(&self.l1.fwd(x)?)?;
|
| 69 |
-
self.l2.fwd(&h)
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
pub fn vars(&self) -> Vec<Var> {
|
| 73 |
-
[self.l1.vars(), self.l2.vars()].concat()
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
/// EMA: `self ← m·self + (1−m)·outro` (usado no codificador-alvo).
|
| 77 |
-
pub fn ema_de(&mut self, outro: &MlpJepa, m: f64) -> Result<()> {
|
| 78 |
-
for (a, b) in self.vars().iter().zip(outro.vars().iter()) {
|
| 79 |
-
let novo = (a.as_tensor().affine(m, 0.0)? + b.as_tensor().affine(1.0 - m, 0.0)?)?;
|
| 80 |
-
a.set(&novo)?;
|
| 81 |
-
}
|
| 82 |
-
Ok(())
|
| 83 |
-
}
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
/// Núcleo JEPA: codificador + preditor + alvo EMA + regularizador VC.
|
| 87 |
-
pub struct NucleoJepa {
|
| 88 |
-
pub encoder: MlpJepa,
|
| 89 |
-
pub preditor: MlpJepa,
|
| 90 |
-
pub alvo: MlpJepa,
|
| 91 |
-
/// tabela de posições (blocos) — entrada extra do preditor.
|
| 92 |
-
pub pos: Var,
|
| 93 |
-
pub d_emb: usize,
|
| 94 |
-
pub d_cond: usize,
|
| 95 |
-
pub m: f64,
|
| 96 |
-
pub lambda_vc: f64,
|
| 97 |
-
pub gamma_vc: f64,
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
impl NucleoJepa {
|
| 101 |
-
pub fn novo(
|
| 102 |
-
d_patch: usize,
|
| 103 |
-
d_emb: usize,
|
| 104 |
-
d_cond: usize,
|
| 105 |
-
n_pos: usize,
|
| 106 |
-
m: f64,
|
| 107 |
-
lambda_vc: f64,
|
| 108 |
-
gamma_vc: f64,
|
| 109 |
-
dev: &Device,
|
| 110 |
-
) -> Result<Self> {
|
| 111 |
-
let encoder = MlpJepa::novo(d_patch, 2 * d_emb, d_emb, dev)?;
|
| 112 |
-
let preditor = MlpJepa::novo(d_emb + d_cond + d_emb, 2 * d_emb, d_emb, dev)?;
|
| 113 |
-
// alvo nasce IGUAL ao encoder (ponto de partida do EMA)
|
| 114 |
-
let mut alvo = MlpJepa::novo(d_patch, 2 * d_emb, d_emb, dev)?;
|
| 115 |
-
for (va, vb) in alvo.vars().iter().zip(encoder.vars().iter()) {
|
| 116 |
-
va.set(vb.as_tensor())?;
|
| 117 |
-
}
|
| 118 |
-
let pos_t = Tensor::randn(0f32, 0.02, (n_pos.max(1), d_emb), dev)?;
|
| 119 |
-
Ok(Self {
|
| 120 |
-
encoder,
|
| 121 |
-
preditor,
|
| 122 |
-
alvo,
|
| 123 |
-
pos: Var::from_tensor(&pos_t)?,
|
| 124 |
-
d_emb,
|
| 125 |
-
d_cond,
|
| 126 |
-
m,
|
| 127 |
-
lambda_vc,
|
| 128 |
-
gamma_vc,
|
| 129 |
-
})
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
pub fn vars(&self) -> Vec<Var> {
|
| 133 |
-
let mut v = self.encoder.vars();
|
| 134 |
-
v.extend(self.preditor.vars());
|
| 135 |
-
v.push(self.pos.clone());
|
| 136 |
-
v // NOTA: alvo NÃO entra — é buffer EMA
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
pub fn ema_passo(&mut self) -> Result<()> {
|
| 140 |
-
self.alvo.ema_de(&self.encoder, self.m)
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
/// Codifica um conjunto de patches (n, d_patch) → (n, d_emb).
|
| 144 |
-
pub fn codificar(&self, patches: &Tensor) -> Result<Tensor> {
|
| 145 |
-
self.encoder.fwd(patches)
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
/// Codifica com o alvo EMA — SEM gradiente (detach por construção: as
|
| 149 |
-
/// vars do alvo não estão no otimizador e o tensor é desconectado).
|
| 150 |
-
pub fn alvo_codificar(&self, patches: &Tensor) -> Result<Tensor> {
|
| 151 |
-
Ok(self.alvo.fwd(patches)?.detach())
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
/// Prediz o embedding do bloco `i_pos` a partir do contexto `z_ctx`
|
| 155 |
-
/// (d_emb) e da condição `cond` (d_cond; zeros se None).
|
| 156 |
-
pub fn predizer(&self, z_ctx: &Tensor, i_pos: usize, cond: Option<&Tensor>) -> Result<Tensor> {
|
| 157 |
-
// z_ctx: (b, d_emb) ou (d_emb,)
|
| 158 |
-
let b = z_ctx.dim(0).unwrap_or(1);
|
| 159 |
-
let dev = z_ctx.device();
|
| 160 |
-
let pos = self.pos.as_tensor().get(i_pos.min(self.pos.dim(0)?.saturating_sub(1)))?;
|
| 161 |
-
let pos_b = pos.broadcast_as((b, self.d_emb))?;
|
| 162 |
-
let cond_b = match cond {
|
| 163 |
-
Some(c) => {
|
| 164 |
-
if c.dims().len() == 1 && b > 1 {
|
| 165 |
-
c.broadcast_as((b, self.d_cond))?
|
| 166 |
-
} else {
|
| 167 |
-
c.clone()
|
| 168 |
-
}
|
| 169 |
-
}
|
| 170 |
-
None => Tensor::zeros((b, self.d_cond), candle_core::DType::F32, dev)?,
|
| 171 |
-
};
|
| 172 |
-
let entrada = Tensor::cat(&[z_ctx, &pos_b, &cond_b], candle_core::D::Minus1)?;
|
| 173 |
-
self.preditor.fwd(&entrada)
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
-
/// Regularizador variância-covariância do lote de incorporações z (n, d).
|
| 177 |
-
///
|
| 178 |
-
/// **J-1:** no colapso total `z_i = c ∀i` ⇒ `std_j = 0` ⇒ `V = d·γ²`.
|
| 179 |
-
pub fn perda_vc(&self, z: &Tensor) -> Result<Tensor> {
|
| 180 |
-
let n = z.dim(0)? as f64;
|
| 181 |
-
let d = self.d_emb;
|
| 182 |
-
let media = z.mean_keepdim(0)?; // (1, d)
|
| 183 |
-
let centr = z.broadcast_sub(&media)?;
|
| 184 |
-
let var = (centr.sqr()?.sum(0)? / n)?; // (d,)
|
| 185 |
-
let std = var.sqrt()?;
|
| 186 |
-
// dobradiça: max(0, γ − std)²
|
| 187 |
-
let gamma_t =
|
| 188 |
-
Tensor::new(self.gamma_vc as f32, z.device())?.broadcast_as(std.dims())?;
|
| 189 |
-
let gap = gamma_t.broadcast_sub(&std)?.clamp(0.0, f64::INFINITY)?;
|
| 190 |
-
let v = gap.sqr()?.sum_all()?;
|
| 191 |
-
// covariância fora-diagonal: C = centrᵀ·centr/n; perda = ‖C − diag‖²_F/(2d)
|
| 192 |
-
let cov = (centr.transpose(0, 1)?.matmul(¢r)? / n)?; // (d, d)
|
| 193 |
-
let mask_diag = {
|
| 194 |
-
let mut m = vec![0f32; d * d];
|
| 195 |
-
for i in 0..d {
|
| 196 |
-
m[i * d + i] = 1.0;
|
| 197 |
-
}
|
| 198 |
-
Tensor::from_vec(m, (d, d), z.device())?
|
| 199 |
-
};
|
| 200 |
-
let fora = ((&cov * &mask_diag)?.sqr()?.sum_all()? / (2.0 * d.max(1) as f64))?;
|
| 201 |
-
let v_norm = (v / d.max(1) as f64)?;
|
| 202 |
-
Ok((v_norm + fora)?)
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
-
/// Perda JEPA padrão para UM lote: contexto (número variável de patches),
|
| 206 |
-
/// lista de blocos-alvo (posições), condição opcional.
|
| 207 |
-
///
|
| 208 |
-
/// `z_ctx` = incorporação agregada do contexto (b, d_emb);
|
| 209 |
-
/// `alvos`: para cada elemento do lote, Vec<(i_pos, patches_alvo)>.
|
| 210 |
-
pub fn perda_lote(
|
| 211 |
-
&self,
|
| 212 |
-
z_ctx: &Tensor,
|
| 213 |
-
alvos: &[Vec<(usize, Tensor)>],
|
| 214 |
-
cond: Option<&Tensor>,
|
| 215 |
-
z_ctx_todos: &Tensor,
|
| 216 |
-
) -> Result<(Tensor, f64, f64)> {
|
| 217 |
-
let b = z_ctx.dim(0)?;
|
| 218 |
-
let dev = z_ctx.device();
|
| 219 |
-
let mut perdas: Vec<Tensor> = Vec::new();
|
| 220 |
-
let mut n_alvos = 0usize;
|
| 221 |
-
for (i, blocos) in alvos.iter().enumerate() {
|
| 222 |
-
for (i_pos, patches_alvo) in blocos {
|
| 223 |
-
// ẑ_b = média das incorporações-alvo dos patches do bloco
|
| 224 |
-
let zt = self.alvo_codificar(patches_alvo)?; // (m, d_emb)
|
| 225 |
-
let zt_m = zt.mean(0)?; // (d_emb,)
|
| 226 |
-
let zt_b = zt_m.broadcast_as((1, self.d_emb))?;
|
| 227 |
-
let zc = z_ctx.get(i)?.broadcast_as((1, self.d_emb))?;
|
| 228 |
-
let c_i = cond.map(|c| c.get(i)?.broadcast_as((1, self.d_cond))).transpose()?;
|
| 229 |
-
let pred = self.predizer(&zc, *i_pos, c_i.as_ref())?;
|
| 230 |
-
let dif = (&pred - &zt_b)?;
|
| 231 |
-
let l = (dif.sqr()?.sum_all()? / self.d_emb as f64)?;
|
| 232 |
-
perdas.push(l);
|
| 233 |
-
n_alvos += 1;
|
| 234 |
-
}
|
| 235 |
-
}
|
| 236 |
-
let l_pred = if perdas.is_empty() {
|
| 237 |
-
Tensor::zeros((), candle_core::DType::F32, &dev)?
|
| 238 |
-
} else {
|
| 239 |
-
(Tensor::stack(&perdas, 0)?.sum_all()? / n_alvos.max(1) as f64)?
|
| 240 |
-
};
|
| 241 |
-
let l_vc = self.perda_vc(z_ctx_todos)?;
|
| 242 |
-
let total = (l_pred.affine(1.0, 0.0)? + l_vc.affine(self.lambda_vc, 0.0)?)?;
|
| 243 |
-
let lp = l_pred.to_scalar::<f32>()? as f64;
|
| 244 |
-
let lv = l_vc.to_scalar::<f32>()? as f64;
|
| 245 |
-
Ok((total, lp, lv))
|
| 246 |
-
}
|
| 247 |
-
}
|
| 248 |
-
|
| 249 |
-
// ---------------------------------------------------------------------------
|
| 250 |
-
// Parte 2 — máscaras
|
| 251 |
-
// ---------------------------------------------------------------------------
|
| 252 |
-
|
| 253 |
-
/// Máscara de blocos 2D (Image-JEPA): grade (h, w) de patches; sorteia
|
| 254 |
-
/// `n_blocos` retângulos aleatórios com fração alvo `frac` de patches.
|
| 255 |
-
pub fn mascara_blocos_2d(h: usize, w: usize, n_blocos: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
|
| 256 |
-
let total = h * w;
|
| 257 |
-
let alvo = (total as f64 * frac) as usize;
|
| 258 |
-
let mut masc = vec![false; total];
|
| 259 |
-
let mut coberto = 0usize;
|
| 260 |
-
for _ in 0..n_blocos.max(1) {
|
| 261 |
-
if coberto >= alvo {
|
| 262 |
-
break;
|
| 263 |
-
}
|
| 264 |
-
let bh = 1 + rng.inteiro(h.min(4));
|
| 265 |
-
let bw = 1 + rng.inteiro(w.min(4));
|
| 266 |
-
let y0 = rng.inteiro(h + 1 - bh);
|
| 267 |
-
let x0 = rng.inteiro(w + 1 - bw);
|
| 268 |
-
for y in y0..y0 + bh {
|
| 269 |
-
for x in x0..x0 + bw {
|
| 270 |
-
let i = y * w + x;
|
| 271 |
-
if !masc[i] {
|
| 272 |
-
masc[i] = true;
|
| 273 |
-
coberto += 1;
|
| 274 |
-
}
|
| 275 |
-
}
|
| 276 |
-
}
|
| 277 |
-
}
|
| 278 |
-
masc
|
| 279 |
-
}
|
| 280 |
-
|
| 281 |
-
/// Máscara de tubos 3D (V-JEPA 2): grade (t, h, w); tubos = retângulos que
|
| 282 |
-
/// atravessam o tempo.
|
| 283 |
-
pub fn mascara_tubos_3d(t: usize, h: usize, w: usize, n_tubos: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
|
| 284 |
-
let total = t * h * w;
|
| 285 |
-
let alvo = (total as f64 * frac) as usize;
|
| 286 |
-
let mut masc = vec![false; total];
|
| 287 |
-
let mut coberto = 0usize;
|
| 288 |
-
for _ in 0..n_tubos.max(1) {
|
| 289 |
-
if coberto >= alvo {
|
| 290 |
-
break;
|
| 291 |
-
}
|
| 292 |
-
let bh = 1 + rng.inteiro(h.min(3));
|
| 293 |
-
let bw = 1 + rng.inteiro(w.min(3));
|
| 294 |
-
let y0 = rng.inteiro(h + 1 - bh);
|
| 295 |
-
let x0 = rng.inteiro(w + 1 - bw);
|
| 296 |
-
for tt in 0..t {
|
| 297 |
-
for y in y0..y0 + bh {
|
| 298 |
-
for x in x0..x0 + bw {
|
| 299 |
-
let i = (tt * h + y) * w + x;
|
| 300 |
-
if !masc[i] {
|
| 301 |
-
masc[i] = true;
|
| 302 |
-
coberto += 1;
|
| 303 |
-
}
|
| 304 |
-
}
|
| 305 |
-
}
|
| 306 |
-
}
|
| 307 |
-
}
|
| 308 |
-
masc
|
| 309 |
-
}
|
| 310 |
-
|
| 311 |
-
/// Máscara de bandas (Audio-JEPA): grade (f, tau) — bandas de frequência
|
| 312 |
-
/// horizontais com extensão temporal.
|
| 313 |
-
pub fn mascara_bandas(f: usize, tau: usize, n_bandas: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
|
| 314 |
-
mascara_blocos_2d(f, tau, n_bandas, frac, rng) // blocos na grade (f, tau)
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
/// Máscara de blocos 3D (3D-JEPA): grade (x, y, z) de voxels.
|
| 318 |
-
pub fn mascara_blocos_3d(x: usize, y: usize, z: usize, n_blocos: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
|
| 319 |
-
mascara_tubos_3d(x, y, z, n_blocos, frac, rng)
|
| 320 |
-
}
|
| 321 |
-
|
| 322 |
-
/// Máscara de segmentos canal×tempo (Brain-JEPA): canais inteiros OU
|
| 323 |
-
/// janelas temporais (aleatório por chamada).
|
| 324 |
-
pub fn mascara_segmentos(c: usize, t: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
|
| 325 |
-
let total = c * t;
|
| 326 |
-
let alvo = (total as f64 * frac) as usize;
|
| 327 |
-
let mut masc = vec![false; total];
|
| 328 |
-
if rng.inteiro(2) == 0 {
|
| 329 |
-
// canais inteiros
|
| 330 |
-
let mut coberto = 0;
|
| 331 |
-
let mut canais: Vec<usize> = (0..c).collect();
|
| 332 |
-
// embaralhar
|
| 333 |
-
for i in (1..canais.len()).rev() {
|
| 334 |
-
let j = rng.inteiro(i + 1);
|
| 335 |
-
canais.swap(i, j);
|
| 336 |
-
}
|
| 337 |
-
for &ch in canais.iter() {
|
| 338 |
-
if coberto >= alvo {
|
| 339 |
-
break;
|
| 340 |
-
}
|
| 341 |
-
for tt in 0..t {
|
| 342 |
-
masc[ch * t + tt] = true;
|
| 343 |
-
coberto += 1;
|
| 344 |
-
}
|
| 345 |
-
}
|
| 346 |
-
} else {
|
| 347 |
-
// janelas temporais
|
| 348 |
-
let mut coberto = 0;
|
| 349 |
-
let mut t0 = 0;
|
| 350 |
-
while coberto < alvo && t0 < t {
|
| 351 |
-
let len = 1 + rng.inteiro((t / 4).max(1));
|
| 352 |
-
for ch in 0..c {
|
| 353 |
-
for tt in t0..(t0 + len).min(t) {
|
| 354 |
-
masc[ch * t + tt] = true;
|
| 355 |
-
coberto += 1;
|
| 356 |
-
}
|
| 357 |
-
}
|
| 358 |
-
t0 += len + 1;
|
| 359 |
-
}
|
| 360 |
-
}
|
| 361 |
-
masc
|
| 362 |
-
}
|
| 363 |
-
|
| 364 |
-
/// Máscara 1D (T-JEPA): intervalos contíguos em n posições.
|
| 365 |
-
pub fn mascara_intervalos_1d(n: usize, frac: f64, rng: &mut RngDeterministico) -> Vec<bool> {
|
| 366 |
-
let alvo = (n as f64 * frac) as usize;
|
| 367 |
-
let mut masc = vec![false; n];
|
| 368 |
-
let mut coberto = 0;
|
| 369 |
-
while coberto < alvo {
|
| 370 |
-
let len = 1 + rng.inteiro((n / 8).max(1));
|
| 371 |
-
let i0 = rng.inteiro(n);
|
| 372 |
-
for i in i0..(i0 + len).min(n) {
|
| 373 |
-
if !masc[i] {
|
| 374 |
-
masc[i] = true;
|
| 375 |
-
coberto += 1;
|
| 376 |
-
}
|
| 377 |
-
}
|
| 378 |
-
}
|
| 379 |
-
masc
|
| 380 |
-
}
|
| 381 |
-
|
| 382 |
-
/// Amostragem dos pontos mais distantes (FPS) para Point-JEPA.
|
| 383 |
-
pub fn pontos_mais_distantes(pontos: &[Vec<f64>], k: usize, rng: &mut RngDeterministico) -> Vec<usize> {
|
| 384 |
-
let n = pontos.len();
|
| 385 |
-
if n == 0 || k == 0 {
|
| 386 |
-
return vec![];
|
| 387 |
-
}
|
| 388 |
-
let k = k.min(n);
|
| 389 |
-
let mut centros = vec![rng.inteiro(n)];
|
| 390 |
-
let mut dist_min: Vec<f64> = pontos
|
| 391 |
-
.iter()
|
| 392 |
-
.map(|p| distancia_eucl(p, &pontos[centros[0]]))
|
| 393 |
-
.collect();
|
| 394 |
-
while centros.len() < k {
|
| 395 |
-
let prox = dist_min
|
| 396 |
-
.iter()
|
| 397 |
-
.enumerate()
|
| 398 |
-
.filter(|(i, _)| !centros.contains(i))
|
| 399 |
-
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
|
| 400 |
-
.map(|(i, _)| i)
|
| 401 |
-
.unwrap_or(0);
|
| 402 |
-
centros.push(prox);
|
| 403 |
-
for (i, p) in pontos.iter().enumerate() {
|
| 404 |
-
let d = distancia_eucl(p, &pontos[prox]);
|
| 405 |
-
if d < dist_min[i] {
|
| 406 |
-
dist_min[i] = d;
|
| 407 |
-
}
|
| 408 |
-
}
|
| 409 |
-
}
|
| 410 |
-
centros
|
| 411 |
-
}
|
| 412 |
-
|
| 413 |
-
fn distancia_eucl(a: &[f64], b: &[f64]) -> f64 {
|
| 414 |
-
a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum::<f64>().sqrt()
|
| 415 |
-
}
|
| 416 |
-
|
| 417 |
-
// ---------------------------------------------------------------------------
|
| 418 |
-
// Parte 3 — adaptadores de modalidade
|
| 419 |
-
// ---------------------------------------------------------------------------
|
| 420 |
-
|
| 421 |
-
/// (C, H, W) → patches (H/ph · W/pw, C·ph·pw) — Image-JEPA.
|
| 422 |
-
pub fn em_patches_2d(img: &Tensor, ph: usize, pw: usize) -> Result<Tensor> {
|
| 423 |
-
let (c, h, w) = (img.dim(0)?, img.dim(1)?, img.dim(2)?);
|
| 424 |
-
let dev = img.device();
|
| 425 |
-
let mut linhas: Vec<Tensor> = Vec::new();
|
| 426 |
-
for y in (0..h).step_by(ph) {
|
| 427 |
-
for x in (0..w).step_by(pw) {
|
| 428 |
-
let hh = ph.min(h - y);
|
| 429 |
-
let ww = pw.min(w - x);
|
| 430 |
-
let patch = img.narrow(0, 0, c)?.narrow(1, y, hh)?.narrow(2, x, ww)?;
|
| 431 |
-
// padding para (C, ph, pw) se borda
|
| 432 |
-
let patch = if hh < ph || ww < pw {
|
| 433 |
-
let mut buf = patch.flatten_all()?.to_vec1::<f32>()?;
|
| 434 |
-
let falta = c * ph * pw - buf.len();
|
| 435 |
-
buf.extend(std::iter::repeat(0f32).take(falta));
|
| 436 |
-
Tensor::from_vec(buf, (c * ph * pw,), &dev)?
|
| 437 |
-
} else {
|
| 438 |
-
patch.flatten_all()?
|
| 439 |
-
};
|
| 440 |
-
linhas.push(patch);
|
| 441 |
-
}
|
| 442 |
-
}
|
| 443 |
-
Ok(Tensor::stack(&linhas, 0)?)
|
| 444 |
-
}
|
| 445 |
-
|
| 446 |
-
/// (T, H, W) → tubos (T/pt · H/ph · W/pw, pt·ph·pw) — V-JEPA 2.
|
| 447 |
-
pub fn em_tubos_3d(vid: &Tensor, pt: usize, ph: usize, pw: usize) -> Result<Tensor> {
|
| 448 |
-
let (t, h, w) = (vid.dim(0)?, vid.dim(1)?, vid.dim(2)?);
|
| 449 |
-
let dev = vid.device();
|
| 450 |
-
let mut linhas: Vec<Tensor> = Vec::new();
|
| 451 |
-
for tt in (0..t).step_by(pt) {
|
| 452 |
-
let tl = pt.min(t - tt);
|
| 453 |
-
for y in (0..h).step_by(ph) {
|
| 454 |
-
for x in (0..w).step_by(pw) {
|
| 455 |
-
let hh = ph.min(h - y);
|
| 456 |
-
let ww = pw.min(w - x);
|
| 457 |
-
let tubo = vid
|
| 458 |
-
.narrow(0, tt, tl)?
|
| 459 |
-
.narrow(1, y, hh)?
|
| 460 |
-
.narrow(2, x, ww)?
|
| 461 |
-
.flatten_all()?;
|
| 462 |
-
let mut buf = tubo.to_vec1::<f32>()?;
|
| 463 |
-
let falta = pt * ph * pw - buf.len();
|
| 464 |
-
buf.extend(std::iter::repeat(0f32).take(falta));
|
| 465 |
-
linhas.push(Tensor::from_vec(buf, (pt * ph * pw,), &dev)?);
|
| 466 |
-
}
|
| 467 |
-
}
|
| 468 |
-
}
|
| 469 |
-
Ok(Tensor::stack(&linhas, 0)?)
|
| 470 |
-
}
|
| 471 |
-
|
| 472 |
-
/// (F, τ) → patches (F/pf · τ/pt, pf·pt) — Audio-JEPA.
|
| 473 |
-
pub fn em_patches_espectrograma(spec: &Tensor, pf: usize, pt: usize) -> Result<Tensor> {
|
| 474 |
-
let (f, tau) = (spec.dim(0)?, spec.dim(1)?);
|
| 475 |
-
let dev = spec.device();
|
| 476 |
-
let mut linhas: Vec<Tensor> = Vec::new();
|
| 477 |
-
for i in (0..f).step_by(pf) {
|
| 478 |
-
for j in (0..tau).step_by(pt) {
|
| 479 |
-
let ff = pf.min(f - i);
|
| 480 |
-
let tt = pt.min(tau - j);
|
| 481 |
-
let p = spec.narrow(0, i, ff)?.narrow(1, j, tt)?.flatten_all()?;
|
| 482 |
-
let mut buf = p.to_vec1::<f32>()?;
|
| 483 |
-
let falta = pf * pt - buf.len();
|
| 484 |
-
buf.extend(std::iter::repeat(0f32).take(falta));
|
| 485 |
-
linhas.push(Tensor::from_vec(buf, (pf * pt,), &dev)?);
|
| 486 |
-
}
|
| 487 |
-
}
|
| 488 |
-
Ok(Tensor::stack(&linhas, 0)?)
|
| 489 |
-
}
|
| 490 |
-
|
| 491 |
-
/// (C, T) → patches canal-independentes (C · T/pt, pt) — T-JEPA/Brain-JEPA.
|
| 492 |
-
pub fn em_patches_serie(ts: &Tensor, pt: usize) -> Result<Tensor> {
|
| 493 |
-
let (c, t) = (ts.dim(0)?, ts.dim(1)?);
|
| 494 |
-
let dev = ts.device();
|
| 495 |
-
let mut linhas: Vec<Tensor> = Vec::new();
|
| 496 |
-
for ch in 0..c {
|
| 497 |
-
for j in (0..t).step_by(pt) {
|
| 498 |
-
let tt = pt.min(t - j);
|
| 499 |
-
let p = ts.narrow(0, ch, 1)?.narrow(1, j, tt)?.flatten_all()?;
|
| 500 |
-
let mut buf = p.to_vec1::<f32>()?;
|
| 501 |
-
let falta = pt - buf.len();
|
| 502 |
-
buf.extend(std::iter::repeat(0f32).take(falta));
|
| 503 |
-
linhas.push(Tensor::from_vec(buf, (pt,), &dev)?);
|
| 504 |
-
}
|
| 505 |
-
}
|
| 506 |
-
Ok(Tensor::stack(&linhas, 0)?)
|
| 507 |
-
}
|
| 508 |
-
|
| 509 |
-
/// (X, Y, Z) → voxels (X/px · Y/py · Z/pz, px·py·pz) — 3D-JEPA.
|
| 510 |
-
pub fn em_voxels(vol: &Tensor, px: usize, py: usize, pz: usize) -> Result<Tensor> {
|
| 511 |
-
let (x, y, z) = (vol.dim(0)?, vol.dim(1)?, vol.dim(2)?);
|
| 512 |
-
let dev = vol.device();
|
| 513 |
-
let mut linhas: Vec<Tensor> = Vec::new();
|
| 514 |
-
for i in (0..x).step_by(px) {
|
| 515 |
-
for j in (0..y).step_by(py) {
|
| 516 |
-
for k in (0..z).step_by(pz) {
|
| 517 |
-
let lx = px.min(x - i);
|
| 518 |
-
let ly = py.min(y - j);
|
| 519 |
-
let lz = pz.min(z - k);
|
| 520 |
-
let p = vol.narrow(0, i, lx)?.narrow(1, j, ly)?.narrow(2, k, lz)?.flatten_all()?;
|
| 521 |
-
let mut buf = p.to_vec1::<f32>()?;
|
| 522 |
-
let falta = px * py * pz - buf.len();
|
| 523 |
-
buf.extend(std::iter::repeat(0f32).take(falta));
|
| 524 |
-
linhas.push(Tensor::from_vec(buf, (px * py * pz,), &dev)?);
|
| 525 |
-
}
|
| 526 |
-
}
|
| 527 |
-
}
|
| 528 |
-
Ok(Tensor::stack(&linhas, 0)?)
|
| 529 |
-
}
|
| 530 |
-
|
| 531 |
-
/// Nuvem de pontos (n, d) → grupos FPS (k, m·d): k centros por FPS, cada
|
| 532 |
-
/// grupo = os m vizinhos mais próximos do centro (concatenados) — Point-JEPA.
|
| 533 |
-
pub fn em_grupos_pontos(pc: &Tensor, k: usize, m: usize, rng: &mut RngDeterministico) -> Result<Tensor> {
|
| 534 |
-
let n = pc.dim(0)?;
|
| 535 |
-
let d = pc.dim(1)?;
|
| 536 |
-
let coords = pc.to_vec2::<f32>()?;
|
| 537 |
-
let coords_f: Vec<Vec<f64>> = coords.iter().map(|r| r.iter().map(|&v| v as f64).collect()).collect();
|
| 538 |
-
let centros = pontos_mais_distantes(&coords_f, k, rng);
|
| 539 |
-
let dev = pc.device();
|
| 540 |
-
let mut linhas: Vec<Tensor> = Vec::new();
|
| 541 |
-
for &ci in ¢ros {
|
| 542 |
-
// m vizinhos mais próximos do centro ci (incluindo ele próprio)
|
| 543 |
-
let mut ord: Vec<usize> = (0..n).collect();
|
| 544 |
-
ord.sort_by(|&a, &b| {
|
| 545 |
-
let da = distancia_eucl(&coords_f[a], &coords_f[ci]);
|
| 546 |
-
let db = distancia_eucl(&coords_f[b], &coords_f[ci]);
|
| 547 |
-
da.partial_cmp(&db).unwrap()
|
| 548 |
-
});
|
| 549 |
-
let mut buf: Vec<f32> = Vec::with_capacity(m * d);
|
| 550 |
-
for &idx in ord.iter().take(m) {
|
| 551 |
-
for j in 0..d {
|
| 552 |
-
buf.push(coords[idx][j]);
|
| 553 |
-
}
|
| 554 |
-
}
|
| 555 |
-
let falta = m * d - buf.len();
|
| 556 |
-
buf.extend(std::iter::repeat(0f32).take(falta));
|
| 557 |
-
linhas.push(Tensor::from_vec(buf, (m * d,), &dev)?);
|
| 558 |
-
}
|
| 559 |
-
Ok(Tensor::stack(&linhas, 0)?)
|
| 560 |
-
}
|
| 561 |
|
| 562 |
// ---------------------------------------------------------------------------
|
| 563 |
// Parte 4 — variantes e treino unificado
|
|
@@ -770,257 +215,6 @@ fn std_por_dim(z: &Tensor) -> Result<f64> {
|
|
| 770 |
Ok(var.sqrt()?.mean_all()?.to_scalar::<f32>()? as f64)
|
| 771 |
}
|
| 772 |
|
| 773 |
-
// ---------------------------------------------------------------------------
|
| 774 |
-
// Parte 5 — SonicMoE (IO/tile-aware)
|
| 775 |
-
// ---------------------------------------------------------------------------
|
| 776 |
-
|
| 777 |
-
/// MoE top-k com capacidade por especialista e perda de balanceamento.
|
| 778 |
-
///
|
| 779 |
-
/// **Prova SM-1 (limite de capacidade):** o roteamento tile-aware mantém no
|
| 780 |
-
/// máximo `C` tokens por especialista por ladrilho — o drop é determinístico
|
| 781 |
-
/// (ordem de chegada) e o nº de tokens descartados é `Σ_e max(0, load_e − C)`.
|
| 782 |
-
///
|
| 783 |
-
/// **Prova SM-2 (balanceamento):** `L_aux = E·Σ_e f_e·p_e`; com roteamento
|
| 784 |
-
/// auto-consistente (`f = p`), `L_aux = E·Σ p_e² ≥ 1` com igualdade sse
|
| 785 |
-
/// `p_e = 1/E` (Cauchy–Schchwarz) — o mínimo força o roteador uniforme.
|
| 786 |
-
pub struct SonicMoe {
|
| 787 |
-
pub roteador: Linear, // d → E
|
| 788 |
-
pub especialistas: Vec<MlpJepa>, // E experts d → d
|
| 789 |
-
pub n_top: usize,
|
| 790 |
-
pub capacidade: usize,
|
| 791 |
-
pub lambda_aux: f64,
|
| 792 |
-
}
|
| 793 |
-
|
| 794 |
-
pub struct SaidaMoe {
|
| 795 |
-
pub saida: Tensor,
|
| 796 |
-
pub perda_aux: Tensor,
|
| 797 |
-
pub carga: Vec<usize>, // tokens efetivamente processados por especialista
|
| 798 |
-
pub descartados: usize, // tokens com especialistas cortados pela capacidade
|
| 799 |
-
}
|
| 800 |
-
|
| 801 |
-
impl SonicMoe {
|
| 802 |
-
pub fn novo(d: usize, n_expertos: usize, n_top: usize, capacidade: usize, dev: &Device) -> Result<Self> {
|
| 803 |
-
let roteador = Linear::sem_vies(d, n_expertos, 0.1, dev)?;
|
| 804 |
-
let especialistas = (0..n_expertos)
|
| 805 |
-
.map(|_| MlpJepa::novo(d, 2 * d, d, dev))
|
| 806 |
-
.collect::<Result<Vec<_>>>()?;
|
| 807 |
-
Ok(Self { roteador, especialistas, n_top, capacidade, lambda_aux: 0.01 })
|
| 808 |
-
}
|
| 809 |
-
|
| 810 |
-
pub fn vars(&self) -> Vec<Var> {
|
| 811 |
-
let mut v = self.roteador.vars();
|
| 812 |
-
for e in &self.especialistas {
|
| 813 |
-
v.extend(e.vars());
|
| 814 |
-
}
|
| 815 |
-
v
|
| 816 |
-
}
|
| 817 |
-
|
| 818 |
-
/// Forward tile-aware: (n, d) → (n, d).
|
| 819 |
-
pub fn fwd(&self, x: &Tensor) -> Result<SaidaMoe> {
|
| 820 |
-
let n = x.dim(0)?;
|
| 821 |
-
let d = x.dim(1)?;
|
| 822 |
-
let e = self.especialistas.len();
|
| 823 |
-
let dev = x.device();
|
| 824 |
-
let logits = self.roteador.fwd(x)?; // (n, E)
|
| 825 |
-
let probs = candle_nn::ops::softmax(&logits, candle_core::D::Minus1)?;
|
| 826 |
-
|
| 827 |
-
// top-k por token (não diferenciável — pesos do top-k são dif. via probs)
|
| 828 |
-
let probs_v = probs.to_vec2::<f32>()?;
|
| 829 |
-
let mut carga = vec![0usize; e];
|
| 830 |
-
let mut descartados = 0usize;
|
| 831 |
-
// acumulador de saída (n, d) — construído por soma de contribuições
|
| 832 |
-
let mut saida = Tensor::zeros((n, d), candle_core::DType::F32, &dev)?;
|
| 833 |
-
let mut peso_total = Tensor::zeros((n, 1), candle_core::DType::F32, &dev)?;
|
| 834 |
-
// por especialista (tile): tokens cujo top-k contém e e com carga < C
|
| 835 |
-
for ei in 0..e {
|
| 836 |
-
// candidatos: tokens ordenados por prob do especialista ei
|
| 837 |
-
let mut cand: Vec<(usize, f32)> = (0..n)
|
| 838 |
-
.map(|i| (i, probs_v[i][ei]))
|
| 839 |
-
.collect();
|
| 840 |
-
cand.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
|
| 841 |
-
let mut escolhidos: Vec<usize> = Vec::new();
|
| 842 |
-
for (i, _) in cand {
|
| 843 |
-
if carga[ei] >= self.capacidade {
|
| 844 |
-
break;
|
| 845 |
-
}
|
| 846 |
-
// verifica se ei está no top-k do token i
|
| 847 |
-
let mut ordem: Vec<(f32, usize)> =
|
| 848 |
-
(0..e).map(|k| (probs_v[i][k], k)).collect();
|
| 849 |
-
ordem.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
|
| 850 |
-
let top: Vec<usize> = ordem.iter().rev().take(self.n_top).map(|(_, k)| *k).collect();
|
| 851 |
-
if top.contains(&ei) {
|
| 852 |
-
escolhidos.push(i);
|
| 853 |
-
carga[ei] += 1;
|
| 854 |
-
}
|
| 855 |
-
}
|
| 856 |
-
if escolhidos.is_empty() {
|
| 857 |
-
continue;
|
| 858 |
-
}
|
| 859 |
-
// tile do especialista: uma única passagem em lote
|
| 860 |
-
let idx: Vec<u32> = escolhidos.iter().map(|&i| i as u32).collect();
|
| 861 |
-
let n_e = idx.len();
|
| 862 |
-
let idx_t = Tensor::from_vec(idx, (n_e,), &dev)?;
|
| 863 |
-
let tile = x.index_select(&idx_t, 0)?;
|
| 864 |
-
let y = self.especialistas[ei].fwd(&tile)?; // (m, d)
|
| 865 |
-
// pesos: prob do especialista ei para cada token escolhido
|
| 866 |
-
let w: Vec<f32> = escolhidos.iter().map(|&i| probs_v[i][ei]).collect();
|
| 867 |
-
let n_w = escolhidos.len();
|
| 868 |
-
let w_t = Tensor::from_vec(w.clone(), (n_w, 1), &dev)?;
|
| 869 |
-
let contrib = y.broadcast_mul(&w_t)?;
|
| 870 |
-
// scatter-add: acumula nas posições
|
| 871 |
-
let mut buf = saida.to_vec2::<f32>()?;
|
| 872 |
-
let contrib_v = contrib.to_vec2::<f32>()?;
|
| 873 |
-
for (k, &i) in escolhidos.iter().enumerate() {
|
| 874 |
-
for j in 0..d {
|
| 875 |
-
buf[i][j] += contrib_v[k][j];
|
| 876 |
-
}
|
| 877 |
-
}
|
| 878 |
-
saida = Tensor::from_vec(
|
| 879 |
-
buf.into_iter().flatten().collect::<Vec<f32>>(),
|
| 880 |
-
(n, d),
|
| 881 |
-
&dev,
|
| 882 |
-
)?;
|
| 883 |
-
let mut pt = peso_total.to_vec2::<f32>()?;
|
| 884 |
-
for (k, &i) in escolhidos.iter().enumerate() {
|
| 885 |
-
pt[i][0] += w[k];
|
| 886 |
-
}
|
| 887 |
-
peso_total = Tensor::from_vec(
|
| 888 |
-
pt.into_iter().flatten().collect::<Vec<f32>>(),
|
| 889 |
-
(n, 1),
|
| 890 |
-
&dev,
|
| 891 |
-
)?;
|
| 892 |
-
}
|
| 893 |
-
// tokens sem especialista (todos cortados) → descartados; saída 0
|
| 894 |
-
let pt_v = peso_total.to_vec2::<f32>()?;
|
| 895 |
-
for i in 0..n {
|
| 896 |
-
if pt_v[i][0] == 0.0 {
|
| 897 |
-
descartados += 1;
|
| 898 |
-
}
|
| 899 |
-
}
|
| 900 |
-
// renormaliza pelo peso efetivo (evita viés da capacidade)
|
| 901 |
-
let denom = peso_total.clamp(1e-6, f64::INFINITY)?;
|
| 902 |
-
let saida = saida.broadcast_div(&denom)?;
|
| 903 |
-
|
| 904 |
-
// perda auxiliar: E·Σ f_e·p_e (f = fração de tokens cujo top-1 é e)
|
| 905 |
-
let mut f = vec![0f32; e];
|
| 906 |
-
for i in 0..n {
|
| 907 |
-
let (mut melhor, mut mp) = (0usize, f32::NEG_INFINITY);
|
| 908 |
-
for k in 0..e {
|
| 909 |
-
if probs_v[i][k] > mp {
|
| 910 |
-
mp = probs_v[i][k];
|
| 911 |
-
melhor = k;
|
| 912 |
-
}
|
| 913 |
-
}
|
| 914 |
-
f[melhor] += 1.0 / n as f32;
|
| 915 |
-
}
|
| 916 |
-
let f_t = Tensor::from_vec(f, (e,), &dev)?;
|
| 917 |
-
let p_media = probs.mean(0)?; // (E,)
|
| 918 |
-
let perda_aux = ((&f_t * &p_media)?.sum_all()? * (e as f64))?;
|
| 919 |
-
Ok(SaidaMoe { saida, perda_aux, carga, descartados })
|
| 920 |
-
}
|
| 921 |
-
}
|
| 922 |
-
|
| 923 |
-
// ---------------------------------------------------------------------------
|
| 924 |
-
// Parte 6 — ReplaySSM
|
| 925 |
-
// ---------------------------------------------------------------------------
|
| 926 |
-
|
| 927 |
-
/// SSM de porta diagonal com replay para aprendizado continuado.
|
| 928 |
-
///
|
| 929 |
-
/// Recorrência: `h_t = σ(a) ⊙ h_{t−1} + (1 − σ(a)) ⊙ (B x_t)`.
|
| 930 |
-
///
|
| 931 |
-
/// **Prova R-1 (estabilidade):** `|σ(a)| < 1` ⇒ perturbações de `h_0`
|
| 932 |
-
/// decaem geometricamente (mesma estrutura do Teo S-2' do SSD-Δ).
|
| 933 |
-
///
|
| 934 |
-
/// **Prova R-2 (limite de esquecimento):** o passo de treino mistura
|
| 935 |
-
/// gradientes `(1−α)·∇L_novo + α·∇L_replay`; para `L_antiga` μ-fortemente
|
| 936 |
-
/// convexa com gradientes novos limitados por `G`, o afastamento do ótimo
|
| 937 |
-
/// antigo por passo é `≤ (1−α)·η·G` (sem replay: `≤ η·G`) — o replay
|
| 938 |
-
/// contrai o drift por um fator `1/(1−α)` no regime linear.
|
| 939 |
-
pub struct ReplaySsm {
|
| 940 |
-
pub a: Var, // (d,) — porta (sigmoid no uso)
|
| 941 |
-
pub b: Var, // (d_in, d) — projeção de entrada
|
| 942 |
-
pub d: usize,
|
| 943 |
-
pub alpha_replay: f64,
|
| 944 |
-
pub buffer: Vec<Tensor>,
|
| 945 |
-
pub capacidade_buffer: usize,
|
| 946 |
-
}
|
| 947 |
-
|
| 948 |
-
impl ReplaySsm {
|
| 949 |
-
pub fn novo(d_in: usize, d: usize, alpha_replay: f64, capacidade_buffer: usize, dev: &Device) -> Result<Self> {
|
| 950 |
-
let a_t = Tensor::randn(0f32, 0.5, (d,), dev)?;
|
| 951 |
-
let b_t = Tensor::randn(0f32, 0.1, (d_in, d), dev)?;
|
| 952 |
-
Ok(Self {
|
| 953 |
-
a: Var::from_tensor(&a_t)?,
|
| 954 |
-
b: Var::from_tensor(&b_t)?,
|
| 955 |
-
d,
|
| 956 |
-
alpha_replay,
|
| 957 |
-
buffer: Vec::new(),
|
| 958 |
-
capacidade_buffer,
|
| 959 |
-
})
|
| 960 |
-
}
|
| 961 |
-
|
| 962 |
-
pub fn vars(&self) -> Vec<Var> {
|
| 963 |
-
vec![self.a.clone(), self.b.clone()]
|
| 964 |
-
}
|
| 965 |
-
|
| 966 |
-
/// Scan: x (T, d_in) → h (T, d). Diferenciável (candle autograd).
|
| 967 |
-
pub fn scan(&self, x: &Tensor) -> Result<Tensor> {
|
| 968 |
-
let t = x.dim(0)?;
|
| 969 |
-
let dev = x.device();
|
| 970 |
-
let porta = candle_nn::ops::sigmoid(&self.a.as_tensor().broadcast_as((1, self.d))?)?; // (1, d)
|
| 971 |
-
let um_menos = porta.affine(-1.0, 1.0)?; // 1 − σ(a)
|
| 972 |
-
let entrada = x.matmul(&self.b.as_tensor())?; // (T, d)
|
| 973 |
-
let mut h = Tensor::zeros((1, self.d), candle_core::DType::F32, &dev)?;
|
| 974 |
-
let mut saidas: Vec<Tensor> = Vec::with_capacity(t);
|
| 975 |
-
for i in 0..t {
|
| 976 |
-
let xt = entrada.narrow(0, i, 1)?;
|
| 977 |
-
h = ((&porta * &h)? + (&um_menos * &xt)?)?;
|
| 978 |
-
saidas.push(h.clone());
|
| 979 |
-
}
|
| 980 |
-
Ok(Tensor::stack(&saidas, 0)?)
|
| 981 |
-
}
|
| 982 |
-
|
| 983 |
-
/// Empurra uma janela de treino no buffer (FIFO).
|
| 984 |
-
pub fn guardar(&mut self, x: &Tensor) {
|
| 985 |
-
if self.buffer.len() >= self.capacidade_buffer {
|
| 986 |
-
self.buffer.remove(0);
|
| 987 |
-
}
|
| 988 |
-
self.buffer.push(x.detach());
|
| 989 |
-
}
|
| 990 |
-
|
| 991 |
-
/// Um passo de treino no lote novo (com mistura de replay).
|
| 992 |
-
/// Devolve (perda_total, perda_novo, perda_replay).
|
| 993 |
-
pub fn passo_treino(
|
| 994 |
-
&mut self,
|
| 995 |
-
x: &Tensor,
|
| 996 |
-
alvo: &Tensor,
|
| 997 |
-
otimizador: &mut crate::treino::AdamWManual,
|
| 998 |
-
) -> Result<(f64, f64, f64)> {
|
| 999 |
-
// perda nova
|
| 1000 |
-
let h = self.scan(x)?;
|
| 1001 |
-
let dif = (&h - alvo)?;
|
| 1002 |
-
let perda_novo = dif.sqr()?.mean_all()?;
|
| 1003 |
-
// replay
|
| 1004 |
-
let mut perda_total = perda_novo.clone();
|
| 1005 |
-
let mut p_r = 0.0;
|
| 1006 |
-
if !self.buffer.is_empty() {
|
| 1007 |
-
let idx = self.buffer.len() - 1;
|
| 1008 |
-
let antigo = self.buffer[idx].clone();
|
| 1009 |
-
let hr = self.scan(&antigo)?;
|
| 1010 |
-
// alvo do replay: manter a dinâmica aprendida estável (magnitudes
|
| 1011 |
-
// finitas) — auto-supervisão de estabilidade
|
| 1012 |
-
let dr = hr.sqr()?.mean_all()?;
|
| 1013 |
-
perda_total = (perda_total.affine(1.0 - self.alpha_replay, 0.0)?
|
| 1014 |
-
+ dr.affine(self.alpha_replay, 0.0)?)?;
|
| 1015 |
-
p_r = dr.to_scalar::<f32>()? as f64;
|
| 1016 |
-
}
|
| 1017 |
-
let pn = perda_novo.to_scalar::<f32>()? as f64;
|
| 1018 |
-
otimizador.passo(crate::treino::grads_achatados(&perda_total, &self.vars())?);
|
| 1019 |
-
self.guardar(x);
|
| 1020 |
-
let pt = perda_total.to_scalar::<f32>()? as f64;
|
| 1021 |
-
Ok((pt, pn, p_r))
|
| 1022 |
-
}
|
| 1023 |
-
}
|
| 1024 |
|
| 1025 |
// ---------------------------------------------------------------------------
|
| 1026 |
// Variantes especiais: LeJEPA (edição-condicionado), MC-JEPA (cruz-modal),
|
|
@@ -1280,210 +474,3 @@ pub fn mascara_por_variante(
|
|
| 1280 |
pub fn versao() -> &'static str {
|
| 1281 |
"jepa-0.5.0"
|
| 1282 |
}
|
| 1283 |
-
|
| 1284 |
-
#[cfg(test)]
|
| 1285 |
-
mod testes {
|
| 1286 |
-
use super::*;
|
| 1287 |
-
|
| 1288 |
-
fn tensor_2d(dados: &[Vec<f64>], dev: &Device) -> Tensor {
|
| 1289 |
-
let flat: Vec<f32> = dados.iter().flat_map(|v| v.iter().map(|&x| x as f32)).collect();
|
| 1290 |
-
Tensor::from_vec(flat, (dados.len(), dados[0].len()), dev).unwrap()
|
| 1291 |
-
}
|
| 1292 |
-
|
| 1293 |
-
#[test]
|
| 1294 |
-
fn teste_mascaras_validas() {
|
| 1295 |
-
// fração respeitada ±, particionamento contexto/alvo não-vazio
|
| 1296 |
-
let mut rng = RngDeterministico::novo(1);
|
| 1297 |
-
let m2 = mascara_blocos_2d(8, 8, 4, 0.3, &mut rng);
|
| 1298 |
-
assert_eq!(m2.len(), 64);
|
| 1299 |
-
let frac = m2.iter().filter(|&&b| b).count() as f64 / 64.0;
|
| 1300 |
-
assert!(frac >= 0.15 && frac <= 0.55, "frac 2d = {frac}");
|
| 1301 |
-
assert!(m2.iter().any(|&b| !b), "contexto vazio");
|
| 1302 |
-
|
| 1303 |
-
let m3 = mascara_tubos_3d(4, 6, 6, 3, 0.3, &mut rng);
|
| 1304 |
-
assert_eq!(m3.len(), 144);
|
| 1305 |
-
let f3 = m3.iter().filter(|&&b| b).count() as f64 / 144.0;
|
| 1306 |
-
assert!(f3 >= 0.15 && f3 <= 0.6, "frac 3d = {f3}");
|
| 1307 |
-
|
| 1308 |
-
let ms = mascara_segmentos(4, 32, 0.25, &mut rng);
|
| 1309 |
-
assert_eq!(ms.len(), 128);
|
| 1310 |
-
assert!(ms.iter().any(|&b| b) && ms.iter().any(|&b| !b));
|
| 1311 |
-
|
| 1312 |
-
let m1 = mascara_intervalos_1d(64, 0.3, &mut rng);
|
| 1313 |
-
let f1 = m1.iter().filter(|&&b| b).count() as f64 / 64.0;
|
| 1314 |
-
assert!(f1 >= 0.25 && f1 <= 0.45, "frac 1d = {f1}");
|
| 1315 |
-
}
|
| 1316 |
-
|
| 1317 |
-
#[test]
|
| 1318 |
-
fn teste_fps_cobre() {
|
| 1319 |
-
let mut rng = RngDeterministico::novo(2);
|
| 1320 |
-
let pontos: Vec<Vec<f64>> = (0..40)
|
| 1321 |
-
.map(|i| {
|
| 1322 |
-
let a = i as f64 * 0.1;
|
| 1323 |
-
vec![a.cos() * 3.0, a.sin() * 3.0, (i % 7) as f64 * 0.3]
|
| 1324 |
-
})
|
| 1325 |
-
.collect();
|
| 1326 |
-
let centros = pontos_mais_distantes(&pontos, 6, &mut rng);
|
| 1327 |
-
assert_eq!(centros.len(), 6);
|
| 1328 |
-
// centros distintos
|
| 1329 |
-
let mut s = centros.clone();
|
| 1330 |
-
s.sort();
|
| 1331 |
-
s.dedup();
|
| 1332 |
-
assert_eq!(s.len(), 6);
|
| 1333 |
-
}
|
| 1334 |
-
|
| 1335 |
-
#[test]
|
| 1336 |
-
fn teste_j1_colapso_impossivel() {
|
| 1337 |
-
// No colapso total (z constante), a perda VC = γ² > 0 — verificando a
|
| 1338 |
-
// fórmula implementada com um lote constante.
|
| 1339 |
-
let dev = Device::Cpu;
|
| 1340 |
-
let nucleo = NucleoJepa::novo(4, 8, 0, 4, 0.99, 1.0, 1.0, &dev).unwrap();
|
| 1341 |
-
let z = Tensor::from_vec(vec![0.5f32; 8 * 8], (8, 8), &dev).unwrap();
|
| 1342 |
-
let lvc = nucleo.perda_vc(&z).unwrap();
|
| 1343 |
-
let v = lvc.to_scalar::<f32>().unwrap() as f64;
|
| 1344 |
-
// std=0 ⇒ dobradiça = γ = 1 ⇒ V/d = 1; cov = 0
|
| 1345 |
-
assert!(v > 0.9, "perda VC no colapso = {v} (deveria ser ~1)");
|
| 1346 |
-
}
|
| 1347 |
-
|
| 1348 |
-
#[test]
|
| 1349 |
-
fn teste_j2_ema_ponto_fixo() {
|
| 1350 |
-
// encoder congelado ⇒ após muitas passadas EMA, alvo = encoder
|
| 1351 |
-
let dev = Device::Cpu;
|
| 1352 |
-
let mut nucleo = NucleoJepa::novo(4, 8, 0, 4, 0.5, 0.1, 1.0, &dev).unwrap();
|
| 1353 |
-
for _ in 0..40 {
|
| 1354 |
-
nucleo.ema_passo().unwrap();
|
| 1355 |
-
}
|
| 1356 |
-
let a = nucleo.alvo.vars()[0].as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap();
|
| 1357 |
-
let b = nucleo.encoder.vars()[0].as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap();
|
| 1358 |
-
let dif: f32 = a.iter().zip(&b).map(|(x, y)| (x - y).abs()).fold(0.0, f32::max);
|
| 1359 |
-
assert!(dif < 1e-5, "EMA não convergiu: dif máx = {dif}");
|
| 1360 |
-
}
|
| 1361 |
-
|
| 1362 |
-
#[test]
|
| 1363 |
-
fn teste_jepa_aprende_latente_previsivel() {
|
| 1364 |
-
// J-3: com alvo previsível (função determinística do contexto), a
|
| 1365 |
-
// perda pred must cair substancialmente e o std das incorporações
|
| 1366 |
-
// NÃO colapsa (J-1).
|
| 1367 |
-
let dev = Device::Cpu;
|
| 1368 |
-
let mut rng = RngDeterministico::novo(3);
|
| 1369 |
-
// patches = 64 vetores 8-D em 4 grupos (estrutura previsível)
|
| 1370 |
-
let mut dados: Vec<Vec<f64>> = Vec::new();
|
| 1371 |
-
for g in 0..4 {
|
| 1372 |
-
let c = g as f64 * 2.0;
|
| 1373 |
-
for _ in 0..16 {
|
| 1374 |
-
dados.push((0..8).map(|k| c + rng.normal(0.0, 0.3) + (k as f64) * 0.05).collect());
|
| 1375 |
-
}
|
| 1376 |
-
}
|
| 1377 |
-
let patches = tensor_2d(&dados, &dev);
|
| 1378 |
-
let cfg = CfgJepa { d_emb: 16, epocas: 40, lr: 5e-3, ..Default::default() };
|
| 1379 |
-
let rel = treinar_variante(
|
| 1380 |
-
&patches,
|
| 1381 |
-
&|n, r| mascara_intervalos_1d(n, 0.3, r),
|
| 1382 |
-
&cfg,
|
| 1383 |
-
VarianteJepa::TJepa,
|
| 1384 |
-
None,
|
| 1385 |
-
&dev,
|
| 1386 |
-
)
|
| 1387 |
-
.unwrap();
|
| 1388 |
-
assert!(
|
| 1389 |
-
rel.perda_final < rel.perda_inicial * 0.8,
|
| 1390 |
-
"perda deveria cair: {} → {}",
|
| 1391 |
-
rel.perda_inicial,
|
| 1392 |
-
rel.perda_final
|
| 1393 |
-
);
|
| 1394 |
-
assert!(
|
| 1395 |
-
rel.std_emb_final > 0.05,
|
| 1396 |
-
"sem colapso: std_final = {}",
|
| 1397 |
-
rel.std_emb_final
|
| 1398 |
-
);
|
| 1399 |
-
}
|
| 1400 |
-
|
| 1401 |
-
#[test]
|
| 1402 |
-
fn teste_adaptadores_formas() {
|
| 1403 |
-
let dev = Device::Cpu;
|
| 1404 |
-
let img = Tensor::randn(0f32, 1.0, (3, 16, 16), &dev).unwrap();
|
| 1405 |
-
let p = em_patches_2d(&img, 4, 4).unwrap();
|
| 1406 |
-
assert_eq!(p.dims().to_vec(), vec![16, 48]);
|
| 1407 |
-
|
| 1408 |
-
let vid = Tensor::randn(0f32, 1.0, (4, 8, 8), &dev).unwrap();
|
| 1409 |
-
let t3 = em_tubos_3d(&vid, 2, 4, 4).unwrap();
|
| 1410 |
-
assert_eq!(t3.dims().to_vec(), vec![8, 32]);
|
| 1411 |
-
|
| 1412 |
-
let spec = Tensor::randn(0f32, 1.0, (16, 32), &dev).unwrap();
|
| 1413 |
-
let ps = em_patches_espectrograma(&spec, 4, 8).unwrap();
|
| 1414 |
-
assert_eq!(ps.dims().to_vec(), vec![16, 32]);
|
| 1415 |
-
|
| 1416 |
-
let ts = Tensor::randn(0f32, 1.0, (4, 32), &dev).unwrap();
|
| 1417 |
-
let pt_ = em_patches_serie(&ts, 8).unwrap();
|
| 1418 |
-
assert_eq!(pt_.dims().to_vec(), vec![16, 8]);
|
| 1419 |
-
|
| 1420 |
-
let vol = Tensor::randn(0f32, 1.0, (8, 8, 8), &dev).unwrap();
|
| 1421 |
-
let vx = em_voxels(&vol, 4, 4, 4).unwrap();
|
| 1422 |
-
assert_eq!(vx.dims().to_vec(), vec![8, 64]);
|
| 1423 |
-
|
| 1424 |
-
let pc = Tensor::randn(0f32, 1.0, (50, 3), &dev).unwrap();
|
| 1425 |
-
let mut rng = RngDeterministico::novo(5);
|
| 1426 |
-
let gr = em_grupos_pontos(&pc, 6, 4, &mut rng).unwrap();
|
| 1427 |
-
assert_eq!(gr.dims().to_vec(), vec![6, 12]);
|
| 1428 |
-
}
|
| 1429 |
-
|
| 1430 |
-
#[test]
|
| 1431 |
-
fn teste_sonic_moe_capacidade_e_balanceamento() {
|
| 1432 |
-
// SM-1: carga por especialista ≤ capacidade; SM-2: aux ≥ ~1 quando
|
| 1433 |
-
// roteador é ~uniforme (início).
|
| 1434 |
-
let dev = Device::Cpu;
|
| 1435 |
-
let moe = SonicMoe::novo(8, 4, 2, 6, &dev).unwrap();
|
| 1436 |
-
let x = Tensor::randn(0f32, 1.0, (20, 8), &dev).unwrap();
|
| 1437 |
-
let s = moe.fwd(&x).unwrap();
|
| 1438 |
-
for (e, c) in s.carga.iter().enumerate() {
|
| 1439 |
-
assert!(*c <= 6, "especialista {e} com carga {c} > capacidade 6");
|
| 1440 |
-
}
|
| 1441 |
-
assert_eq!(s.saida.dims().to_vec(), vec![20, 8]);
|
| 1442 |
-
let aux = s.perda_aux.to_scalar::<f32>().unwrap() as f64;
|
| 1443 |
-
// com roteador quase uniforme, aux ≈ E·Σ (1/E)·(1/E) = 1
|
| 1444 |
-
assert!(aux > 0.5 && aux < 3.0, "aux = {aux}");
|
| 1445 |
-
// cada token processado por ≤ n_top especialistas
|
| 1446 |
-
let total_carga: usize = s.carga.iter().sum();
|
| 1447 |
-
assert!(total_carga <= 20 * 2, "carga total {total_carga} > n·topk");
|
| 1448 |
-
}
|
| 1449 |
-
|
| 1450 |
-
#[test]
|
| 1451 |
-
fn teste_replay_ssm_estabilidade_e_replay() {
|
| 1452 |
-
// R-1: estados finitos e limitados; R-2: com replay α>0, após treinar
|
| 1453 |
-
// a tarefa nova, a dinâmica antiga (magnitude do estado) permanece
|
| 1454 |
-
// estável (não diverge).
|
| 1455 |
-
let dev = Device::Cpu;
|
| 1456 |
-
let mut ssm = ReplaySsm::novo(4, 6, 0.5, 8, &dev).unwrap();
|
| 1457 |
-
let vars = ssm.vars();
|
| 1458 |
-
let mut otim = crate::treino::AdamWManual::novo(vars, 1e-2, 0.0);
|
| 1459 |
-
|
| 1460 |
-
// tarefa "antiga": sinal constante 1.0
|
| 1461 |
-
let x_antigo = Tensor::from_vec(vec![1.0f32; 16 * 4], (16, 4), &dev).unwrap();
|
| 1462 |
-
for _ in 0..30 {
|
| 1463 |
-
let h = ssm.scan(&x_antigo).unwrap();
|
| 1464 |
-
let alvo = h.detach();
|
| 1465 |
-
ssm.passo_treino(&x_antigo, &alvo, &mut otim).unwrap();
|
| 1466 |
-
}
|
| 1467 |
-
// estado após treino antigo: finito
|
| 1468 |
-
let h1 = ssm.scan(&x_antigo).unwrap();
|
| 1469 |
-
let m1 = h1.abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
|
| 1470 |
-
assert!(m1.is_finite() && m1 < 50.0, "estado explodiu: {m1}");
|
| 1471 |
-
|
| 1472 |
-
// tarefa "nova": sinal −1.0 — treino com replay
|
| 1473 |
-
let x_novo = Tensor::from_vec(vec![-1.0f32; 16 * 4], (16, 4), &dev).unwrap();
|
| 1474 |
-
for _ in 0..30 {
|
| 1475 |
-
let h = ssm.scan(&x_novo).unwrap();
|
| 1476 |
-
let alvo = h.detach();
|
| 1477 |
-
ssm.passo_treino(&x_novo, &alvo, &mut otim).unwrap();
|
| 1478 |
-
}
|
| 1479 |
-
// a dinâmica antiga continua estável (sem explosão)
|
| 1480 |
-
let h2 = ssm.scan(&x_antigo).unwrap();
|
| 1481 |
-
let m2 = h2.abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
|
| 1482 |
-
assert!(m2.is_finite() && m2 < 50.0, "estado antigo explodiu: {m2}");
|
| 1483 |
-
// portas dentro de (0,1)
|
| 1484 |
-
let a = ssm.a.as_tensor().to_vec1::<f32>().unwrap();
|
| 1485 |
-
for v in a {
|
| 1486 |
-
assert!(v.is_finite());
|
| 1487 |
-
}
|
| 1488 |
-
}
|
| 1489 |
-
}
|
|
|
|
| 1 |
+
//! `jepa::variantes` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
+
use super::*;
|
|
|
|
|
|
|
|
|
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
// ---------------------------------------------------------------------------
|
| 8 |
// Parte 4 — variantes e treino unificado
|
|
|
|
| 215 |
Ok(var.sqrt()?.mean_all()?.to_scalar::<f32>()? as f64)
|
| 216 |
}
|
| 217 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
// ---------------------------------------------------------------------------
|
| 220 |
// Variantes especiais: LeJEPA (edição-condicionado), MC-JEPA (cruz-modal),
|
|
|
|
| 474 |
pub fn versao() -> &'static str {
|
| 475 |
"jepa-0.5.0"
|
| 476 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/lib.rs
CHANGED
|
@@ -11,6 +11,7 @@ pub mod arvore_pesos;
|
|
| 11 |
pub mod atencao;
|
| 12 |
pub mod acoplador;
|
| 13 |
pub mod camadas;
|
|
|
|
| 14 |
pub mod config;
|
| 15 |
pub mod controlador;
|
| 16 |
pub mod diagnostico;
|
|
|
|
| 11 |
pub mod atencao;
|
| 12 |
pub mod acoplador;
|
| 13 |
pub mod camadas;
|
| 14 |
+
pub mod checkpoint;
|
| 15 |
pub mod config;
|
| 16 |
pub mod controlador;
|
| 17 |
pub mod diagnostico;
|
src/moe_redirecionavel/atencao.rs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `moe_redirecionavel::atencao` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
// ---------------------------------------------------------------------------
|
| 8 |
+
// Atenção própria (independente e paralela à ADBA do tronco)
|
| 9 |
+
// ---------------------------------------------------------------------------
|
| 10 |
+
|
| 11 |
+
pub struct AtencaoPropria {
|
| 12 |
+
pub q: Linear,
|
| 13 |
+
pub k: Linear,
|
| 14 |
+
pub v: Linear,
|
| 15 |
+
pub o: Linear,
|
| 16 |
+
/// gate escalar — nasce ZERO (M-R2: neutralidade no nascimento).
|
| 17 |
+
pub gate: Var,
|
| 18 |
+
pub n_cabecas: usize,
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
impl AtencaoPropria {
|
| 22 |
+
pub fn novo(d: usize, n_cabecas: usize, dev: &Device) -> Result<Self> {
|
| 23 |
+
let desvio = 1.0 / ((d * 2) as f64).sqrt();
|
| 24 |
+
let mk = || Linear::sem_vies(d, d, desvio, dev);
|
| 25 |
+
let gate = Tensor::zeros((), candle_core::DType::F32, dev)?;
|
| 26 |
+
Ok(AtencaoPropria {
|
| 27 |
+
q: mk()?, k: mk()?, v: mk()?, o: mk()?,
|
| 28 |
+
gate: Var::from_tensor(&gate)?,
|
| 29 |
+
n_cabecas,
|
| 30 |
+
})
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/// x (n, d) → y = x + g·attn(x) — multi-cabeça compacta (sem máscara:
|
| 34 |
+
/// atenção plena entre os tokens da entrada do MoE).
|
| 35 |
+
pub fn fwd(&self, x: &Tensor) -> Result<Tensor> {
|
| 36 |
+
let d = x.dim(candle_core::D::Minus1)?;
|
| 37 |
+
let d_cab = d / self.n_cabecas;
|
| 38 |
+
let n = x.dim(0)?;
|
| 39 |
+
let forma = (self.n_cabecas, n, d_cab);
|
| 40 |
+
let q = self.q.fwd(x)?.reshape(forma)?;
|
| 41 |
+
let k = self.k.fwd(x)?.reshape(forma)?;
|
| 42 |
+
let v = self.v.fwd(x)?.reshape(forma)?;
|
| 43 |
+
let scores = q.matmul(&k.transpose(1, 2)?)?.affine(1.0 / (d_cab as f64).sqrt(), 0.0)?;
|
| 44 |
+
let attn = candle_nn::ops::softmax(&scores, candle_core::D::Minus1)?;
|
| 45 |
+
let ctx = attn.matmul(&v)?; // (h, n, d_cab)
|
| 46 |
+
let ctx = ctx.reshape((n, d))?;
|
| 47 |
+
let saida = self.o.fwd(&ctx)?;
|
| 48 |
+
// y = x + g·saida (g=0 no nascimento ⇒ y = x — M-R2)
|
| 49 |
+
let g = self.gate.as_tensor();
|
| 50 |
+
let forma: Vec<usize> = saida.dims().to_vec();
|
| 51 |
+
let delta = saida.broadcast_mul(&g.broadcast_as(forma)?)?;
|
| 52 |
+
Ok((x + &delta)?)
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
pub fn vars(&self) -> Vec<Var> {
|
| 56 |
+
let mut v = self.q.vars();
|
| 57 |
+
v.extend(self.k.vars());
|
| 58 |
+
v.extend(self.v.vars());
|
| 59 |
+
v.extend(self.o.vars());
|
| 60 |
+
v.push(self.gate.clone());
|
| 61 |
+
v
|
| 62 |
+
}
|
| 63 |
+
}
|
src/moe_redirecionavel/mod.rs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! MoE redirecionável — módulos: atenção própria, núcleo,
|
| 2 |
+
//! treinos JEPA/SOM (divisão SRP v0.10.0 — r-35 §2).
|
| 3 |
+
|
| 4 |
+
//! **MoEs de codificação/decodificação REDIRECIONÁVEIS com Mecanismo de
|
| 5 |
+
//! Atenção próprio e independente (paralelo)** (v0.6.0 — pedido do usuário).
|
| 6 |
+
//!
|
| 7 |
+
//! Cada `MoeRedirecionavel` possui:
|
| 8 |
+
//! - um roteador aprendido `d → E`;
|
| 9 |
+
//! - E especialistas `MlpJepa`, cada um com um PAPEL (Codificador ou
|
| 10 |
+
//! Decodificador) — o conjunto de papéis é REDIRECIONÁVEL em tempo de
|
| 11 |
+
//! execução (permutação), sem reconstrução do modelo;
|
| 12 |
+
//! - uma ATENÇÃO PRÓPRIA independente da atenção ADBA do tronco, com gate
|
| 13 |
+
//! escalar nascido em ZERO (neutralidade no nascimento);
|
| 14 |
+
//! - capacidade por especialista e perda auxiliar de balanceamento.
|
| 15 |
+
//!
|
| 16 |
+
//! **Provas completas em `docs/memorias/r-25-*`; síntese:**
|
| 17 |
+
//! - **M-R1 (invariância sob redirecionamento):** o redirecionamento é uma
|
| 18 |
+
//! permutação π dos papéis; o multiconjunto de atribuições
|
| 19 |
+
//! (especialista, token) numa passada por papel é levado ao multiconjunto
|
| 20 |
+
//! da passada π(papel) — a capacidade total e a perda auxiliar de
|
| 21 |
+
//! balanceamento E·Σ f_e·p_e são invariantes (soma sobre todos os e).
|
| 22 |
+
//! - **M-R2 (neutralidade da atenção própria no nascimento):** com
|
| 23 |
+
//! `g = 0`, `y = x` bit-a-bit; a perturbação é limitada por
|
| 24 |
+
//! `‖Δy‖ ≤ L_soft·|g|·‖W_o·V‖` (Lipschitz do softmax com constante
|
| 25 |
+
//! L_soft = 1 em probabilidade; estrutura idêntica ao T-P5).
|
| 26 |
+
//! - **M-R3 (paralelismo dos dois caminhos):** numa mesma entrada, as
|
| 27 |
+
//! passadas Codificador e Decodificador tocam conjuntos DISJUNTOS de
|
| 28 |
+
//! especialistas e compartilham apenas leitura — o DAG de computação tem
|
| 29 |
+
//! duas sub-DAGs independentes ⇒ tempo paralelo (2 processadores) =
|
| 30 |
+
//! max(T_enc, T_dec) + sobrecarga < T_enc + T_dec = tempo sequencial
|
| 31 |
+
//! (modelo PRAM; medido empiricamente com threads).
|
| 32 |
+
//! - **M-R4 (fechamento da classe de funções):** redirecionar equivale a
|
| 33 |
+
//! permutar colunas do roteador e re-rotular papéis; a classe de funções
|
| 34 |
+
//! implementáveis é fechada sob π ⇒ expressividade inalterada.
|
| 35 |
+
//! - **M-R5 (water-filling por QE — Cauchy–Schwarz):** dadas capacidades
|
| 36 |
+
//! totais C e erros por célula q_i, a alocação `c_i ∝ q_i` minimiza
|
| 37 |
+
//! `Σ q_i²/c_i ≥ (Σq_i)²/C`, cota superior do erro refinado ponderado.
|
| 38 |
+
|
| 39 |
+
use candle_core::{Device, Result, Tensor, Var};
|
| 40 |
+
use crate::camadas::Linear;
|
| 41 |
+
use crate::jepa::MlpJepa;
|
| 42 |
+
use crate::rng::RngDeterministico;
|
| 43 |
+
use crate::treino::AdamWManual;
|
| 44 |
+
|
| 45 |
+
// ---- divisão SRP v0.10.0 (r-35 §2): submódulos + re-exports ----
|
| 46 |
+
#[cfg(test)]
|
| 47 |
+
mod testes;
|
| 48 |
+
#[cfg(test)]
|
| 49 |
+
mod teste_integrado;
|
| 50 |
+
mod atencao;
|
| 51 |
+
mod nucleo;
|
| 52 |
+
mod treino;
|
| 53 |
+
|
| 54 |
+
pub use atencao::*;
|
| 55 |
+
pub use nucleo::*;
|
| 56 |
+
pub use treino::*;
|
| 57 |
+
|
src/{moe_redirecionavel.rs → moe_redirecionavel/nucleo.rs}
RENAMED
|
@@ -1,101 +1,8 @@
|
|
| 1 |
-
//!
|
| 2 |
-
//!
|
| 3 |
-
//!
|
| 4 |
-
//! Cada `MoeRedirecionavel` possui:
|
| 5 |
-
//! - um roteador aprendido `d → E`;
|
| 6 |
-
//! - E especialistas `MlpJepa`, cada um com um PAPEL (Codificador ou
|
| 7 |
-
//! Decodificador) — o conjunto de papéis é REDIRECIONÁVEL em tempo de
|
| 8 |
-
//! execução (permutação), sem reconstrução do modelo;
|
| 9 |
-
//! - uma ATENÇÃO PRÓPRIA independente da atenção ADBA do tronco, com gate
|
| 10 |
-
//! escalar nascido em ZERO (neutralidade no nascimento);
|
| 11 |
-
//! - capacidade por especialista e perda auxiliar de balanceamento.
|
| 12 |
-
//!
|
| 13 |
-
//! **Provas completas em `docs/memorias/r-25-*`; síntese:**
|
| 14 |
-
//! - **M-R1 (invariância sob redirecionamento):** o redirecionamento é uma
|
| 15 |
-
//! permutação π dos papéis; o multiconjunto de atribuições
|
| 16 |
-
//! (especialista, token) numa passada por papel é levado ao multiconjunto
|
| 17 |
-
//! da passada π(papel) — a capacidade total e a perda auxiliar de
|
| 18 |
-
//! balanceamento E·Σ f_e·p_e são invariantes (soma sobre todos os e).
|
| 19 |
-
//! - **M-R2 (neutralidade da atenção própria no nascimento):** com
|
| 20 |
-
//! `g = 0`, `y = x` bit-a-bit; a perturbação é limitada por
|
| 21 |
-
//! `‖Δy‖ ≤ L_soft·|g|·‖W_o·V‖` (Lipschitz do softmax com constante
|
| 22 |
-
//! L_soft = 1 em probabilidade; estrutura idêntica ao T-P5).
|
| 23 |
-
//! - **M-R3 (paralelismo dos dois caminhos):** numa mesma entrada, as
|
| 24 |
-
//! passadas Codificador e Decodificador tocam conjuntos DISJUNTOS de
|
| 25 |
-
//! especialistas e compartilham apenas leitura — o DAG de computação tem
|
| 26 |
-
//! duas sub-DAGs independentes ⇒ tempo paralelo (2 processadores) =
|
| 27 |
-
//! max(T_enc, T_dec) + sobrecarga < T_enc + T_dec = tempo sequencial
|
| 28 |
-
//! (modelo PRAM; medido empiricamente com threads).
|
| 29 |
-
//! - **M-R4 (fechamento da classe de funções):** redirecionar equivale a
|
| 30 |
-
//! permutar colunas do roteador e re-rotular papéis; a classe de funções
|
| 31 |
-
//! implementáveis é fechada sob π ⇒ expressividade inalterada.
|
| 32 |
-
//! - **M-R5 (water-filling por QE — Cauchy–Schwarz):** dadas capacidades
|
| 33 |
-
//! totais C e erros por célula q_i, a alocação `c_i ∝ q_i` minimiza
|
| 34 |
-
//! `Σ q_i²/c_i ≥ (Σq_i)²/C`, cota superior do erro refinado ponderado.
|
| 35 |
|
| 36 |
-
use
|
| 37 |
-
use crate::camadas::Linear;
|
| 38 |
-
use crate::jepa::MlpJepa;
|
| 39 |
-
use crate::rng::RngDeterministico;
|
| 40 |
-
use crate::treino::AdamWManual;
|
| 41 |
|
| 42 |
-
// ---------------------------------------------------------------------------
|
| 43 |
-
// Atenção própria (independente e paralela à ADBA do tronco)
|
| 44 |
-
// ---------------------------------------------------------------------------
|
| 45 |
-
|
| 46 |
-
pub struct AtencaoPropria {
|
| 47 |
-
pub q: Linear,
|
| 48 |
-
pub k: Linear,
|
| 49 |
-
pub v: Linear,
|
| 50 |
-
pub o: Linear,
|
| 51 |
-
/// gate escalar — nasce ZERO (M-R2: neutralidade no nascimento).
|
| 52 |
-
pub gate: Var,
|
| 53 |
-
pub n_cabecas: usize,
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
impl AtencaoPropria {
|
| 57 |
-
pub fn novo(d: usize, n_cabecas: usize, dev: &Device) -> Result<Self> {
|
| 58 |
-
let desvio = 1.0 / ((d * 2) as f64).sqrt();
|
| 59 |
-
let mk = || Linear::sem_vies(d, d, desvio, dev);
|
| 60 |
-
let gate = Tensor::zeros((), candle_core::DType::F32, dev)?;
|
| 61 |
-
Ok(AtencaoPropria {
|
| 62 |
-
q: mk()?, k: mk()?, v: mk()?, o: mk()?,
|
| 63 |
-
gate: Var::from_tensor(&gate)?,
|
| 64 |
-
n_cabecas,
|
| 65 |
-
})
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
/// x (n, d) → y = x + g·attn(x) — multi-cabeça compacta (sem máscara:
|
| 69 |
-
/// atenção plena entre os tokens da entrada do MoE).
|
| 70 |
-
pub fn fwd(&self, x: &Tensor) -> Result<Tensor> {
|
| 71 |
-
let d = x.dim(candle_core::D::Minus1)?;
|
| 72 |
-
let d_cab = d / self.n_cabecas;
|
| 73 |
-
let n = x.dim(0)?;
|
| 74 |
-
let forma = (self.n_cabecas, n, d_cab);
|
| 75 |
-
let q = self.q.fwd(x)?.reshape(forma)?;
|
| 76 |
-
let k = self.k.fwd(x)?.reshape(forma)?;
|
| 77 |
-
let v = self.v.fwd(x)?.reshape(forma)?;
|
| 78 |
-
let scores = q.matmul(&k.transpose(1, 2)?)?.affine(1.0 / (d_cab as f64).sqrt(), 0.0)?;
|
| 79 |
-
let attn = candle_nn::ops::softmax(&scores, candle_core::D::Minus1)?;
|
| 80 |
-
let ctx = attn.matmul(&v)?; // (h, n, d_cab)
|
| 81 |
-
let ctx = ctx.reshape((n, d))?;
|
| 82 |
-
let saida = self.o.fwd(&ctx)?;
|
| 83 |
-
// y = x + g·saida (g=0 no nascimento ⇒ y = x — M-R2)
|
| 84 |
-
let g = self.gate.as_tensor();
|
| 85 |
-
let forma: Vec<usize> = saida.dims().to_vec();
|
| 86 |
-
let delta = saida.broadcast_mul(&g.broadcast_as(forma)?)?;
|
| 87 |
-
Ok((x + &delta)?)
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
pub fn vars(&self) -> Vec<Var> {
|
| 91 |
-
let mut v = self.q.vars();
|
| 92 |
-
v.extend(self.k.vars());
|
| 93 |
-
v.extend(self.v.vars());
|
| 94 |
-
v.extend(self.o.vars());
|
| 95 |
-
v.push(self.gate.clone());
|
| 96 |
-
v
|
| 97 |
-
}
|
| 98 |
-
}
|
| 99 |
|
| 100 |
// ---------------------------------------------------------------------------
|
| 101 |
// MoE redirecionável
|
|
@@ -522,686 +429,3 @@ pub fn alocar_por_qe(qes: &[f64], capacidade_total: usize) -> Vec<usize> {
|
|
| 522 |
}
|
| 523 |
c
|
| 524 |
}
|
| 525 |
-
|
| 526 |
-
// ---------------------------------------------------------------------------
|
| 527 |
-
// Aplicação JEPA — codificador/preditor redirecionáveis com MoE
|
| 528 |
-
// ---------------------------------------------------------------------------
|
| 529 |
-
|
| 530 |
-
pub struct RelatorioJepaMoe {
|
| 531 |
-
pub modo: &'static str,
|
| 532 |
-
pub perda_pred_inicial: f64,
|
| 533 |
-
pub perda_pred_final: f64,
|
| 534 |
-
pub perda_vc_final: f64,
|
| 535 |
-
pub std_emb: f64,
|
| 536 |
-
pub carga_max: usize,
|
| 537 |
-
pub descartados: usize,
|
| 538 |
-
}
|
| 539 |
-
|
| 540 |
-
/// Treino JEPA compacto com três braços (ablação honesta):
|
| 541 |
-
/// - `"pequena"`: codificador/preditor MLP simples (baseline);
|
| 542 |
-
/// - `"densa"`: MLP com o TOTAL de parâmetros dos especialistas (capacidade
|
| 543 |
-
/// equiparada, SEM roteamento — isola o ganho do roteamento);
|
| 544 |
-
/// - `"moe"`: `MoeRedirecionavel` nos dois caminhos (Codificador no
|
| 545 |
-
/// codificador, Decodificador no preditor) com atenção própria.
|
| 546 |
-
pub fn treinar_jepa_com_moe(
|
| 547 |
-
patches: &Tensor,
|
| 548 |
-
modo: &'static str,
|
| 549 |
-
passos: usize,
|
| 550 |
-
frac_mascara: f64,
|
| 551 |
-
semente: u64,
|
| 552 |
-
) -> Result<RelatorioJepaMoe> {
|
| 553 |
-
let dev = patches.device();
|
| 554 |
-
let n = patches.dim(0)?;
|
| 555 |
-
let d = patches.dim(1)?;
|
| 556 |
-
let mut rng = RngDeterministico::novo(semente);
|
| 557 |
-
// alvo = patches originais (detach); contexto = patches mascarados
|
| 558 |
-
let alvo = patches.detach();
|
| 559 |
-
let contexto = {
|
| 560 |
-
let mut v = patches.flatten_all()?.to_vec1::<f32>()?;
|
| 561 |
-
for x in v.iter_mut() {
|
| 562 |
-
if rng.uniforme() < frac_mascara {
|
| 563 |
-
*x = 0.0;
|
| 564 |
-
}
|
| 565 |
-
}
|
| 566 |
-
Tensor::from_vec(v, (n, d), &dev)?
|
| 567 |
-
};
|
| 568 |
-
let e = 4usize; // especialistas por papel no braço moe
|
| 569 |
-
let n_top = 2;
|
| 570 |
-
let capacidade = n;
|
| 571 |
-
|
| 572 |
-
// braços
|
| 573 |
-
enum Braco {
|
| 574 |
-
Pequena(MlpJepa, MlpJepa),
|
| 575 |
-
Densa(MlpJepa, MlpJepa),
|
| 576 |
-
Moe(MoeRedirecionavel),
|
| 577 |
-
}
|
| 578 |
-
let braco = match modo {
|
| 579 |
-
"pequena" => Braco::Pequena(
|
| 580 |
-
MlpJepa::novo(d, 2 * d, d, &dev)?,
|
| 581 |
-
MlpJepa::novo(d, 2 * d, d, &dev)?,
|
| 582 |
-
),
|
| 583 |
-
"densa" => {
|
| 584 |
-
// capacidade equiparada: oculta = soma das ocultas dos especialistas
|
| 585 |
-
let d_oculta = 2 * d * e;
|
| 586 |
-
Braco::Densa(
|
| 587 |
-
MlpJepa::novo(d, d_oculta, d, &dev)?,
|
| 588 |
-
MlpJepa::novo(d, d_oculta, d, &dev)?,
|
| 589 |
-
)
|
| 590 |
-
}
|
| 591 |
-
_ => Braco::Moe(MoeRedirecionavel::novo(d, 2 * e, n_top, capacidade, &dev)?),
|
| 592 |
-
};
|
| 593 |
-
|
| 594 |
-
let perda_de = |b: &Braco, x: &Tensor| -> Result<(Tensor, Tensor, usize)> {
|
| 595 |
-
match b {
|
| 596 |
-
Braco::Pequena(e1, p1) => {
|
| 597 |
-
let z = e1.fwd(x)?;
|
| 598 |
-
let yh = p1.fwd(&z)?;
|
| 599 |
-
let dif = (&yh - &alvo)?;
|
| 600 |
-
Ok(((dif.sqr()?.sum_all()? / (n as f64 * d as f64))?,
|
| 601 |
-
z.sqr()?.mean_all()?, 0))
|
| 602 |
-
}
|
| 603 |
-
Braco::Densa(e1, p1) => {
|
| 604 |
-
let z = e1.fwd(x)?;
|
| 605 |
-
let yh = p1.fwd(&z)?;
|
| 606 |
-
let dif = (&yh - &alvo)?;
|
| 607 |
-
Ok(((dif.sqr()?.sum_all()? / (n as f64 * d as f64))?,
|
| 608 |
-
z.sqr()?.mean_all()?, 0))
|
| 609 |
-
}
|
| 610 |
-
Braco::Moe(m) => {
|
| 611 |
-
// v0.9.0: forward (ex-fwd_vetorizado) — gradiente flui aos
|
| 612 |
-
// especialistas e ao roteador (a fwd seccionava o grafo)
|
| 613 |
-
let s_enc = m.forward(x, Papel::Codificador)?;
|
| 614 |
-
let s_dec = m.forward(&s_enc.saida, Papel::Decodificador)?;
|
| 615 |
-
let dif = (&s_dec.saida - &alvo)?;
|
| 616 |
-
let lp = (dif.sqr()?.sum_all()? / (n as f64 * d as f64))?;
|
| 617 |
-
// perda total = preditiva + aux balanceamento (ambos papéis)
|
| 618 |
-
let lt = (lp.affine(1.0, 0.0)? + s_enc.perda_aux.affine(m.lambda_aux, 0.0)?
|
| 619 |
-
+ s_dec.perda_aux.affine(m.lambda_aux, 0.0)?)?;
|
| 620 |
-
let carga_max = s_enc.carga.iter().copied().fold(0, usize::max)
|
| 621 |
-
.max(s_dec.carga.iter().copied().fold(0, usize::max));
|
| 622 |
-
Ok((lt, lp, carga_max))
|
| 623 |
-
}
|
| 624 |
-
}
|
| 625 |
-
};
|
| 626 |
-
|
| 627 |
-
let (mut opt, vars): (AdamWManual, Vec<Var>) = match &braco {
|
| 628 |
-
Braco::Pequena(a, b) => {
|
| 629 |
-
let v = [a.vars(), b.vars()].concat();
|
| 630 |
-
(AdamWManual::novo(v.clone(), 3e-3, 0.0), v)
|
| 631 |
-
}
|
| 632 |
-
Braco::Densa(a, b) => {
|
| 633 |
-
let v = [a.vars(), b.vars()].concat();
|
| 634 |
-
(AdamWManual::novo(v.clone(), 3e-3, 0.0), v)
|
| 635 |
-
}
|
| 636 |
-
Braco::Moe(m) => (AdamWManual::novo(m.vars(), 3e-3, 0.0), m.vars()),
|
| 637 |
-
};
|
| 638 |
-
// atenção própria: gate nasce 0; sobe 1 passo em 8 (aquecimento honesto
|
| 639 |
-
// do mecanismo — T-P5 análogo: neutralidade no nascimento, gradiente flui)
|
| 640 |
-
if let Braco::Moe(m) = &braco {
|
| 641 |
-
let g0 = m.atencao.gate.as_tensor().to_scalar::<f32>()? as f64;
|
| 642 |
-
let _ = g0;
|
| 643 |
-
}
|
| 644 |
-
|
| 645 |
-
let (l0, _, _) = perda_de(&braco, &contexto)?;
|
| 646 |
-
let perda_inicial = l0.to_scalar::<f32>()? as f64;
|
| 647 |
-
let mut perda_final = perda_inicial;
|
| 648 |
-
let mut vc_final = 0.0;
|
| 649 |
-
let mut carga_max = 0usize;
|
| 650 |
-
let mut descartados = 0usize;
|
| 651 |
-
let mut std_emb = 0.0;
|
| 652 |
-
for passo in 0..passos {
|
| 653 |
-
let (lt, lp, cm) = perda_de(&braco, &contexto)?;
|
| 654 |
-
if let Braco::Moe(m) = &braco {
|
| 655 |
-
// abre o gate gradualmente (0 → 1 em 25% dos passos)
|
| 656 |
-
let alvo_g = ((passo as f64 / (passos as f64 / 4.0).max(1.0)).min(1.0)) as f32;
|
| 657 |
-
let g_atual = m.atencao.gate.as_tensor().to_scalar::<f32>()? as f64;
|
| 658 |
-
if (alvo_g as f64 - g_atual).abs() > 1e-9 {
|
| 659 |
-
m.atencao.gate.set(&Tensor::new(alvo_g, &dev)?)?;
|
| 660 |
-
}
|
| 661 |
-
}
|
| 662 |
-
let grads = crate::treino::grads_achatados(<, &vars)?;
|
| 663 |
-
AdamWManual::clipar(&mut grads.clone(), 1.0);
|
| 664 |
-
let mut g2 = grads;
|
| 665 |
-
AdamWManual::clipar(&mut g2, 5.0);
|
| 666 |
-
opt.passo(g2);
|
| 667 |
-
perda_final = lp.to_scalar::<f32>()? as f64;
|
| 668 |
-
carga_max = cm;
|
| 669 |
-
// VC / std do embedding (para o relatório)
|
| 670 |
-
let (_, vc, _) = perda_de(&braco, &contexto)?;
|
| 671 |
-
vc_final = vc.to_scalar::<f32>()? as f64;
|
| 672 |
-
std_emb = {
|
| 673 |
-
let z = match &braco {
|
| 674 |
-
Braco::Pequena(a, _) => a.fwd(&contexto)?,
|
| 675 |
-
Braco::Densa(a, _) => a.fwd(&contexto)?,
|
| 676 |
-
Braco::Moe(m) => m.forward(&contexto, Papel::Codificador)?.saida,
|
| 677 |
-
};
|
| 678 |
-
z.flatten_all()?.to_vec1::<f32>()?.iter().map(|x| *x as f64).collect::<Vec<f64>>()
|
| 679 |
-
.chunks(n).map(|c| {
|
| 680 |
-
let m = c.iter().sum::<f64>() / c.len() as f64;
|
| 681 |
-
(c.iter().map(|x| (x - m).powi(2)).sum::<f64>() / c.len() as f64).sqrt()
|
| 682 |
-
}).sum::<f64>() / n as f64
|
| 683 |
-
};
|
| 684 |
-
}
|
| 685 |
-
if let Braco::Moe(m) = &braco {
|
| 686 |
-
let s = m.forward(&contexto, Papel::Codificador)?;
|
| 687 |
-
descartados = s.descartados;
|
| 688 |
-
}
|
| 689 |
-
Ok(RelatorioJepaMoe {
|
| 690 |
-
modo,
|
| 691 |
-
perda_pred_inicial: perda_inicial,
|
| 692 |
-
perda_pred_final: perda_final,
|
| 693 |
-
perda_vc_final: vc_final,
|
| 694 |
-
std_emb,
|
| 695 |
-
carga_max,
|
| 696 |
-
descartados,
|
| 697 |
-
})
|
| 698 |
-
}
|
| 699 |
-
|
| 700 |
-
// ---------------------------------------------------------------------------
|
| 701 |
-
// Aplicação SOM — refinamento residual com alocação por QE (M-R5)
|
| 702 |
-
// ---------------------------------------------------------------------------
|
| 703 |
-
|
| 704 |
-
pub struct RelatorioSomMoe {
|
| 705 |
-
pub qe_antes: f64,
|
| 706 |
-
pub qe_depois: f64,
|
| 707 |
-
pub qe_alocacao_uniforme: f64,
|
| 708 |
-
pub carga_por_celula: Vec<usize>,
|
| 709 |
-
pub tempo_treino_s: f64,
|
| 710 |
-
/// qualidade da DECODIFICAÇÃO após redirecionamento (mse w_bmu → x).
|
| 711 |
-
pub mse_decodificador: f64,
|
| 712 |
-
}
|
| 713 |
-
|
| 714 |
-
/// Refina um SOM 4D treinado com um MoE redirecionável:
|
| 715 |
-
/// 1. aloca especialistas por célula ∝ QE (water-filling — M-R5);
|
| 716 |
-
/// 2. treina o papel Codificador como corretor residual
|
| 717 |
-
/// `x̂ = w_bmu(x) + E(x)`;
|
| 718 |
-
/// 3. mede QE′; repete com alocação uniforme (ablação do water-filling);
|
| 719 |
-
/// 4. REDIRECIONA os especialistas para Decodificador e treina
|
| 720 |
-
/// `x̂ = E(w_bmu)` (reconstrução a partir do código) — mede o MSE.
|
| 721 |
-
pub fn refinar_som_com_moe(
|
| 722 |
-
dados: &Tensor,
|
| 723 |
-
centroides: &[Vec<f64>],
|
| 724 |
-
passos: usize,
|
| 725 |
-
semente: u64,
|
| 726 |
-
) -> Result<RelatorioSomMoe> {
|
| 727 |
-
use std::time::Instant;
|
| 728 |
-
let t0 = Instant::now();
|
| 729 |
-
let dev = dados.device();
|
| 730 |
-
let n = dados.dim(0)?;
|
| 731 |
-
let d = dados.dim(1)?;
|
| 732 |
-
let m = centroides.len();
|
| 733 |
-
// BMU + QE por célula (tolera F32/F64)
|
| 734 |
-
let dados_v: Vec<Vec<f64>> = match dados.to_vec2::<f64>() {
|
| 735 |
-
Ok(v) => v,
|
| 736 |
-
Err(_) => dados
|
| 737 |
-
.to_vec2::<f32>()?
|
| 738 |
-
.iter()
|
| 739 |
-
.map(|l| l.iter().map(|x| *x as f64).collect())
|
| 740 |
-
.collect(),
|
| 741 |
-
};
|
| 742 |
-
let bmu: Vec<usize> = dados_v
|
| 743 |
-
.iter()
|
| 744 |
-
.map(|x| {
|
| 745 |
-
centroides
|
| 746 |
-
.iter()
|
| 747 |
-
.enumerate()
|
| 748 |
-
.min_by(|(i, a), (j, b)| {
|
| 749 |
-
let da: f64 = a.iter().zip(x).map(|(w, v)| (w - v).powi(2)).sum();
|
| 750 |
-
let db: f64 = b.iter().zip(x).map(|(w, v)| (w - v).powi(2)).sum();
|
| 751 |
-
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal).then(i.cmp(j))
|
| 752 |
-
})
|
| 753 |
-
.map(|(i, _)| i)
|
| 754 |
-
.unwrap_or(0)
|
| 755 |
-
})
|
| 756 |
-
.collect();
|
| 757 |
-
let mut qe_cel = vec![0f64; m];
|
| 758 |
-
let mut cont_cel = vec![0usize; m];
|
| 759 |
-
for (i, b) in bmu.iter().enumerate() {
|
| 760 |
-
let dist: f64 = centroides[*b].iter().zip(&dados_v[i]).map(|(w, v)| (w - v).powi(2)).sum();
|
| 761 |
-
qe_cel[*b] += dist.sqrt();
|
| 762 |
-
cont_cel[*b] += 1;
|
| 763 |
-
}
|
| 764 |
-
for i in 0..m {
|
| 765 |
-
if cont_cel[i] > 0 {
|
| 766 |
-
qe_cel[i] /= cont_cel[i] as f64;
|
| 767 |
-
}
|
| 768 |
-
}
|
| 769 |
-
let qe_antes: f64 = dados_v
|
| 770 |
-
.iter()
|
| 771 |
-
.enumerate()
|
| 772 |
-
.map(|(i, b)| {
|
| 773 |
-
let w = ¢roides[bmu[i]];
|
| 774 |
-
(w.iter().zip(b).map(|(a, v)| (a - v).powi(2)).sum::<f64>()).sqrt()
|
| 775 |
-
})
|
| 776 |
-
.sum::<f64>()
|
| 777 |
-
/ n as f64;
|
| 778 |
-
|
| 779 |
-
// ---------- braço water-filling ----------
|
| 780 |
-
// capacidade = m células + 8 especialistas EXTRA distribuídos ∝ QE
|
| 781 |
-
// (com C < m o water-filling degenera em uniforme — mínimo 1 por célula)
|
| 782 |
-
let capacidade_total = m + 8;
|
| 783 |
-
let aloca_wf = alocar_por_qe(&qe_cel, capacidade_total);
|
| 784 |
-
let qe_wf = treinar_corretor(dados, centroides, &bmu, &aloca_wf, passos, semente, &dev)?;
|
| 785 |
-
// ---------- braço uniforme (ablação: MESMO total e MESMA semente —
|
| 786 |
-
// única diferença é a alocação; round-robin soma exatamente C) ----------
|
| 787 |
-
let aloca_un = {
|
| 788 |
-
let mut a = vec![1usize; m];
|
| 789 |
-
let mut i = 0usize;
|
| 790 |
-
while a.iter().sum::<usize>() < capacidade_total {
|
| 791 |
-
a[i % m] += 1;
|
| 792 |
-
i += 1;
|
| 793 |
-
}
|
| 794 |
-
a
|
| 795 |
-
};
|
| 796 |
-
let qe_un = treinar_corretor(dados, centroides, &bmu, &aloca_un, passos, semente, &dev)?;
|
| 797 |
-
|
| 798 |
-
// ---------- redirecionamento → decodificador ----------
|
| 799 |
-
// mesma pool de especialistas, agora aprendendo w_bmu → x
|
| 800 |
-
let mse_dec = treinar_decodificador(dados, centroides, &bmu, &aloca_wf, passos, semente + 2, &dev)?;
|
| 801 |
-
|
| 802 |
-
Ok(RelatorioSomMoe {
|
| 803 |
-
qe_antes,
|
| 804 |
-
qe_depois: qe_wf,
|
| 805 |
-
qe_alocacao_uniforme: qe_un,
|
| 806 |
-
carga_por_celula: aloca_wf,
|
| 807 |
-
tempo_treino_s: t0.elapsed().as_secs_f64(),
|
| 808 |
-
mse_decodificador: mse_dec,
|
| 809 |
-
})
|
| 810 |
-
}
|
| 811 |
-
|
| 812 |
-
/// Corretor residual: x̂ = w_bmu + E_{célula}(x). Treina com AdamW.
|
| 813 |
-
fn treinar_corretor(
|
| 814 |
-
dados: &Tensor,
|
| 815 |
-
centroides: &[Vec<f64>],
|
| 816 |
-
bmu: &[usize],
|
| 817 |
-
alocacao: &[usize],
|
| 818 |
-
passos: usize,
|
| 819 |
-
semente: u64,
|
| 820 |
-
dev: &Device,
|
| 821 |
-
) -> Result<f64> {
|
| 822 |
-
let n = dados.dim(0)?;
|
| 823 |
-
let d = dados.dim(1)?;
|
| 824 |
-
// um "especialista" = MlpJepa por CÉLULA (capacidade alocada = nº de
|
| 825 |
-
// especialistas extra treinados nessa célula, somados em ensemble)
|
| 826 |
-
let n_esp_tot: usize = alocacao.iter().sum();
|
| 827 |
-
let mut ensembles: Vec<Vec<MlpJepa>> = Vec::with_capacity(centroides.len());
|
| 828 |
-
for c in alocacao {
|
| 829 |
-
let mut v = Vec::new();
|
| 830 |
-
for _ in 0..(*c).max(1) {
|
| 831 |
-
v.push(MlpJepa::novo(d, 4 * d, d, dev)?);
|
| 832 |
-
}
|
| 833 |
-
ensembles.push(v);
|
| 834 |
-
}
|
| 835 |
-
let mut vars: Vec<Var> = Vec::new();
|
| 836 |
-
for ens in &ensembles {
|
| 837 |
-
for e in ens {
|
| 838 |
-
vars.extend(e.vars());
|
| 839 |
-
}
|
| 840 |
-
}
|
| 841 |
-
let mut opt = AdamWManual::novo(vars.clone(), 3e-3, 0.0);
|
| 842 |
-
// tensores de apoio
|
| 843 |
-
let w_bmu = {
|
| 844 |
-
let v: Vec<f32> = (0..n).flat_map(|i| centroides[bmu[i]].iter().map(|x| *x as f32).collect::<Vec<f32>>()).collect();
|
| 845 |
-
Tensor::from_vec(v, (n, d), dev)?
|
| 846 |
-
};
|
| 847 |
-
let alvo_res = (dados - &w_bmu)?;
|
| 848 |
-
for _ in 0..passos {
|
| 849 |
-
let mut soma = Tensor::zeros((), candle_core::DType::F32, dev)?;
|
| 850 |
-
for (i, ens) in ensembles.iter().enumerate() {
|
| 851 |
-
// tokens da célula i
|
| 852 |
-
let idx: Vec<u32> = (0..n).filter(|&k| bmu[k] == i).map(|k| k as u32).collect();
|
| 853 |
-
if idx.is_empty() {
|
| 854 |
-
continue;
|
| 855 |
-
}
|
| 856 |
-
let m_idx = idx.len();
|
| 857 |
-
let it = Tensor::from_vec(idx, (m_idx,), dev)?;
|
| 858 |
-
let x_c = dados.index_select(&it, 0)?;
|
| 859 |
-
let r_c = alvo_res.index_select(&it, 0)?;
|
| 860 |
-
// ensemble: média das saídas
|
| 861 |
-
let mut y = Tensor::zeros((m_idx, d), candle_core::DType::F32, dev)?;
|
| 862 |
-
for e in ens {
|
| 863 |
-
y = (y.affine(1.0, 0.0)? + e.fwd(&x_c)?.affine(1.0, 0.0)?)?;
|
| 864 |
-
}
|
| 865 |
-
let k = ens.len() as f64;
|
| 866 |
-
let yh = y.affine(1.0 / k, 0.0)?;
|
| 867 |
-
let dif = (&yh - &r_c)?;
|
| 868 |
-
soma = (soma.affine(1.0, 0.0)? + dif.sqr()?.sum_all()?.affine(1.0 / (n as f64 * d as f64), 0.0)?)?;
|
| 869 |
-
}
|
| 870 |
-
let _ = n_esp_tot;
|
| 871 |
-
let grads = crate::treino::grads_achatados(&soma, &vars)?;
|
| 872 |
-
let mut g = grads;
|
| 873 |
-
AdamWManual::clipar(&mut g, 5.0);
|
| 874 |
-
opt.passo(g);
|
| 875 |
-
}
|
| 876 |
-
// QE refinado
|
| 877 |
-
let mut qe = 0f64;
|
| 878 |
-
for (i, ens) in ensembles.iter().enumerate() {
|
| 879 |
-
let idx: Vec<u32> = (0..n).filter(|&k| bmu[k] == i).map(|k| k as u32).collect();
|
| 880 |
-
if idx.is_empty() {
|
| 881 |
-
continue;
|
| 882 |
-
}
|
| 883 |
-
let m_idx = idx.len();
|
| 884 |
-
let it = Tensor::from_vec(idx, (m_idx,), dev)?;
|
| 885 |
-
let x_c = dados.index_select(&it, 0)?;
|
| 886 |
-
let mut y = Tensor::zeros((m_idx, d), candle_core::DType::F32, dev)?;
|
| 887 |
-
for e in ens {
|
| 888 |
-
y = (y.affine(1.0, 0.0)? + e.fwd(&x_c)?.affine(1.0, 0.0)?)?;
|
| 889 |
-
}
|
| 890 |
-
let k = ens.len() as f64;
|
| 891 |
-
let yh = y.affine(1.0 / k, 0.0)?;
|
| 892 |
-
let xhat = (w_bmu.index_select(&it, 0)? + &yh)?;
|
| 893 |
-
let dif = (&x_c - &xhat)?;
|
| 894 |
-
qe += dif.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
|
| 895 |
-
}
|
| 896 |
-
Ok((qe / (n as f64 * d as f64)).sqrt() * (d as f64).sqrt()) // QE médio por ponto (norma L2)
|
| 897 |
-
}
|
| 898 |
-
|
| 899 |
-
/// Decodificador (pós-redirecionamento): x̂ = E(w_bmu) — reconstrução
|
| 900 |
-
/// a partir apenas do código (centróide). Mede o MSE final.
|
| 901 |
-
fn treinar_decodificador(
|
| 902 |
-
dados: &Tensor,
|
| 903 |
-
centroides: &[Vec<f64>],
|
| 904 |
-
bmu: &[usize],
|
| 905 |
-
alocacao: &[usize],
|
| 906 |
-
passos: usize,
|
| 907 |
-
semente: u64,
|
| 908 |
-
dev: &Device,
|
| 909 |
-
) -> Result<f64> {
|
| 910 |
-
let _ = semente;
|
| 911 |
-
let n = dados.dim(0)?;
|
| 912 |
-
let d = dados.dim(1)?;
|
| 913 |
-
let mut ensembles: Vec<Vec<MlpJepa>> = Vec::with_capacity(centroides.len());
|
| 914 |
-
for c in alocacao {
|
| 915 |
-
let mut v = Vec::new();
|
| 916 |
-
for _ in 0..(*c).max(1) {
|
| 917 |
-
v.push(MlpJepa::novo(d, 4 * d, d, dev)?);
|
| 918 |
-
}
|
| 919 |
-
ensembles.push(v);
|
| 920 |
-
}
|
| 921 |
-
let mut vars: Vec<Var> = Vec::new();
|
| 922 |
-
for ens in &ensembles {
|
| 923 |
-
for e in ens {
|
| 924 |
-
vars.extend(e.vars());
|
| 925 |
-
}
|
| 926 |
-
}
|
| 927 |
-
let mut opt = AdamWManual::novo(vars.clone(), 3e-3, 0.0);
|
| 928 |
-
let w_bmu = {
|
| 929 |
-
let v: Vec<f32> = (0..n).flat_map(|i| centroides[bmu[i]].iter().map(|x| *x as f32).collect::<Vec<f32>>()).collect();
|
| 930 |
-
Tensor::from_vec(v, (n, d), dev)?
|
| 931 |
-
};
|
| 932 |
-
for _ in 0..passos {
|
| 933 |
-
let mut soma = Tensor::zeros((), candle_core::DType::F32, dev)?;
|
| 934 |
-
for (i, ens) in ensembles.iter().enumerate() {
|
| 935 |
-
let idx: Vec<u32> = (0..n).filter(|&k| bmu[k] == i).map(|k| k as u32).collect();
|
| 936 |
-
if idx.is_empty() {
|
| 937 |
-
continue;
|
| 938 |
-
}
|
| 939 |
-
let m_idx = idx.len();
|
| 940 |
-
let it = Tensor::from_vec(idx, (m_idx,), dev)?;
|
| 941 |
-
let w_c = w_bmu.index_select(&it, 0)?;
|
| 942 |
-
let x_c = dados.index_select(&it, 0)?;
|
| 943 |
-
let mut y = Tensor::zeros((m_idx, d), candle_core::DType::F32, dev)?;
|
| 944 |
-
for e in ens {
|
| 945 |
-
y = (y.affine(1.0, 0.0)? + e.fwd(&w_c)?.affine(1.0, 0.0)?)?;
|
| 946 |
-
}
|
| 947 |
-
let k = ens.len() as f64;
|
| 948 |
-
let yh = y.affine(1.0 / k, 0.0)?;
|
| 949 |
-
let dif = (&yh - &x_c)?;
|
| 950 |
-
soma = (soma.affine(1.0, 0.0)? + dif.sqr()?.sum_all()?.affine(1.0 / (n as f64 * d as f64), 0.0)?)?;
|
| 951 |
-
}
|
| 952 |
-
let grads = crate::treino::grads_achatados(&soma, &vars)?;
|
| 953 |
-
let mut g = grads;
|
| 954 |
-
AdamWManual::clipar(&mut g, 5.0);
|
| 955 |
-
opt.passo(g);
|
| 956 |
-
}
|
| 957 |
-
// MSE final do decodificador
|
| 958 |
-
let mut mse = 0f64;
|
| 959 |
-
for (i, ens) in ensembles.iter().enumerate() {
|
| 960 |
-
let idx: Vec<u32> = (0..n).filter(|&k| bmu[k] == i).map(|k| k as u32).collect();
|
| 961 |
-
if idx.is_empty() {
|
| 962 |
-
continue;
|
| 963 |
-
}
|
| 964 |
-
let m_idx = idx.len();
|
| 965 |
-
let it = Tensor::from_vec(idx, (m_idx,), dev)?;
|
| 966 |
-
let w_c = w_bmu.index_select(&it, 0)?;
|
| 967 |
-
let x_c = dados.index_select(&it, 0)?;
|
| 968 |
-
let mut y = Tensor::zeros((m_idx, d), candle_core::DType::F32, dev)?;
|
| 969 |
-
for e in ens {
|
| 970 |
-
y = (y.affine(1.0, 0.0)? + e.fwd(&w_c)?.affine(1.0, 0.0)?)?;
|
| 971 |
-
}
|
| 972 |
-
let k = ens.len() as f64;
|
| 973 |
-
let yh = y.affine(1.0 / k, 0.0)?;
|
| 974 |
-
let dif = (&yh - &x_c)?;
|
| 975 |
-
mse += dif.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
|
| 976 |
-
}
|
| 977 |
-
Ok(mse / (n as f64 * d as f64))
|
| 978 |
-
}
|
| 979 |
-
|
| 980 |
-
pub fn versao() -> &'static str {
|
| 981 |
-
"moe-redirecionavel-0.6.0"
|
| 982 |
-
}
|
| 983 |
-
|
| 984 |
-
#[cfg(test)]
|
| 985 |
-
#[allow(deprecated)] // testes usam o oráculo fwd (bug v080-1) de propósito
|
| 986 |
-
mod testes {
|
| 987 |
-
use super::*;
|
| 988 |
-
|
| 989 |
-
fn dados_sinteticos(n: usize, d: usize, semente: u64) -> Tensor {
|
| 990 |
-
let mut rng = crate::rng::RngDeterministico::novo(semente);
|
| 991 |
-
let v: Vec<f32> = (0..n * d).map(|_| rng.normal(0.0, 1.0) as f32).collect();
|
| 992 |
-
Tensor::from_vec(v, (n, d), &Device::Cpu).unwrap()
|
| 993 |
-
}
|
| 994 |
-
|
| 995 |
-
#[test]
|
| 996 |
-
fn mr1_invariancia_sob_redirecionamento() {
|
| 997 |
-
let dev = Device::Cpu;
|
| 998 |
-
let x = dados_sinteticos(64, 16, 3);
|
| 999 |
-
let mut moe = MoeRedirecionavel::novo(16, 6, 2, 64, &dev).unwrap();
|
| 1000 |
-
let s1 = moe.fwd(&x, Papel::Codificador).unwrap();
|
| 1001 |
-
let perda_antes = s1.perda_aux.to_scalar::<f32>().unwrap() as f64;
|
| 1002 |
-
let carga_antes: usize = s1.carga.iter().sum();
|
| 1003 |
-
moe.redirecionar_todos();
|
| 1004 |
-
// após trocar TODOS os papéis, a passada Decodificador usa os
|
| 1005 |
-
// especialistas que eram do codificador — a perda auxiliar do
|
| 1006 |
-
// SUBCONJUNTO é comparável (mesma distribuição de roteamento)
|
| 1007 |
-
let s2 = moe.fwd(&x, Papel::Decodificador).unwrap();
|
| 1008 |
-
let perda_depois = s2.perda_aux.to_scalar::<f32>().unwrap() as f64;
|
| 1009 |
-
let carga_depois: usize = s2.carga.iter().sum();
|
| 1010 |
-
assert_eq!(carga_antes, carga_depois, "capacidade total conservada (M-R1)");
|
| 1011 |
-
assert!(
|
| 1012 |
-
(perda_antes - perda_depois).abs() < 2.0 + 1e-6,
|
| 1013 |
-
"perda auxiliar da família conservada: {perda_antes:.4} vs {perda_depois:.4}"
|
| 1014 |
-
);
|
| 1015 |
-
}
|
| 1016 |
-
|
| 1017 |
-
#[test]
|
| 1018 |
-
fn mr2_neutralidade_no_nascimento() {
|
| 1019 |
-
let dev = Device::Cpu;
|
| 1020 |
-
let x = dados_sinteticos(32, 8, 5);
|
| 1021 |
-
let moe = MoeRedirecionavel::novo(8, 4, 2, 32, &dev).unwrap();
|
| 1022 |
-
let g = moe.atencao.gate.as_tensor().to_scalar::<f32>().unwrap();
|
| 1023 |
-
assert!(g == 0.0, "gate nasce zero");
|
| 1024 |
-
let y = moe.atencao.fwd(&x).unwrap();
|
| 1025 |
-
let dif = (&y - &x).unwrap().sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap();
|
| 1026 |
-
assert!(dif == 0.0, "y = x bit-a-bit no nascimento (dif={dif})");
|
| 1027 |
-
}
|
| 1028 |
-
|
| 1029 |
-
#[test]
|
| 1030 |
-
fn mr3_paralelismo_e_determinismo() {
|
| 1031 |
-
let dev = Device::Cpu;
|
| 1032 |
-
let x = dados_sinteticos(96, 24, 7);
|
| 1033 |
-
let moe = MoeRedirecionavel::novo(24, 8, 2, 96, &dev).unwrap();
|
| 1034 |
-
let (_enc, _dec, t_par, t_seq, speedup) = moe.fwd_ambos_par(&x).unwrap();
|
| 1035 |
-
// determinismo já garantido por assert interno; speedup registrado
|
| 1036 |
-
assert!(t_par > 0.0 && t_seq > 0.0);
|
| 1037 |
-
let _ = speedup;
|
| 1038 |
-
}
|
| 1039 |
-
|
| 1040 |
-
#[test]
|
| 1041 |
-
fn mr5_water_filling_minimiza_cota() {
|
| 1042 |
-
// cota Σ q_i²/c_i: water-filling ≤ uniforme (Cauchy–Schwarz)
|
| 1043 |
-
let qes = vec![0.2, 0.5, 1.0, 0.3];
|
| 1044 |
-
let c_wf = alocar_por_qe(&qes, 8);
|
| 1045 |
-
let c_un = vec![2usize; 4];
|
| 1046 |
-
let f = |c: &[usize]| qes.iter().zip(c).map(|(q, ci)| q * q / *ci as f64).sum::<f64>();
|
| 1047 |
-
assert!(
|
| 1048 |
-
f(&c_wf) <= f(&c_un) + 1e-9,
|
| 1049 |
-
"water-filling {c_wf:?} (cota {:.4}) deve ≤ uniforme (cota {:.4})",
|
| 1050 |
-
f(&c_wf), f(&c_un)
|
| 1051 |
-
);
|
| 1052 |
-
assert_eq!(c_wf.iter().sum::<usize>(), 8);
|
| 1053 |
-
}
|
| 1054 |
-
|
| 1055 |
-
#[test]
|
| 1056 |
-
fn jepa_moe_aprende() {
|
| 1057 |
-
let x = dados_sinteticos(64, 12, 11);
|
| 1058 |
-
let r = treinar_jepa_com_moe(&x, "moe", 60, 0.5, 42).unwrap();
|
| 1059 |
-
assert!(r.perda_pred_final < r.perda_pred_inicial, "perda preditiva cai");
|
| 1060 |
-
}
|
| 1061 |
-
|
| 1062 |
-
/// **M-R6 (v0.8.0):** equivalência de VALORES entre `fwd` e
|
| 1063 |
-
/// `forward` — mesma seleção (auxiliar compartilhado), mesma
|
| 1064 |
-
/// saída, mesma perda auxiliar, mesma carga/descartados.
|
| 1065 |
-
#[test]
|
| 1066 |
-
fn mr6_equivalencia_de_valores() {
|
| 1067 |
-
let dev = Device::Cpu;
|
| 1068 |
-
for (n, d, e, k, cap, seed) in [
|
| 1069 |
-
(64usize, 16usize, 6usize, 2usize, 64usize, 3u64),
|
| 1070 |
-
(96, 24, 8, 2, 96, 5),
|
| 1071 |
-
(48, 12, 4, 1, 24, 9), // capacidade apertada: força descartados
|
| 1072 |
-
] {
|
| 1073 |
-
let x = dados_sinteticos(n, d, seed);
|
| 1074 |
-
let moe = MoeRedirecionavel::novo(d, e, k, cap, &dev).unwrap();
|
| 1075 |
-
let s_ref = moe.fwd(&x, Papel::Codificador).unwrap();
|
| 1076 |
-
let s_vet = moe.forward(&x, Papel::Codificador).unwrap();
|
| 1077 |
-
assert_eq!(s_ref.carga, s_vet.carga, "M-R6: mesma carga (n={n})");
|
| 1078 |
-
assert_eq!(s_ref.usados, s_vet.usados, "M-R6: mesmos usados");
|
| 1079 |
-
assert_eq!(s_ref.descartados, s_vet.descartados, "M-R6: mesmos descartados");
|
| 1080 |
-
let dif = (&s_ref.saida - &s_vet.saida).unwrap()
|
| 1081 |
-
.abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
|
| 1082 |
-
assert!(dif < 1e-4, "M-R6: max|Δsaída| = {dif} (n={n}, cap={cap})");
|
| 1083 |
-
let pa = s_ref.perda_aux.to_scalar::<f32>().unwrap();
|
| 1084 |
-
let pb = s_vet.perda_aux.to_scalar::<f32>().unwrap();
|
| 1085 |
-
assert!(
|
| 1086 |
-
(pa - pb).abs() < 1e-4,
|
| 1087 |
-
"M-R6: perda auxiliar {pa:.6} vs {pb:.6} (n={n})"
|
| 1088 |
-
);
|
| 1089 |
-
}
|
| 1090 |
-
}
|
| 1091 |
-
|
| 1092 |
-
/// **M-R7 (v0.8.0):** restauração do fluxo de gradiente — bug v080-1.
|
| 1093 |
-
/// Com a `fwd` (referência de valores) os especialistas e o roteador
|
| 1094 |
-
/// NÃO recebem gradiente (grafo seccionado por to_vec2/from_vec); com
|
| 1095 |
-
/// a `forward` TODOS recebem (index_add/index_select vivos).
|
| 1096 |
-
#[test]
|
| 1097 |
-
fn mr7_restauracao_do_gradiente() {
|
| 1098 |
-
let dev = Device::Cpu;
|
| 1099 |
-
let x = dados_sinteticos(48, 12, 13);
|
| 1100 |
-
let moe = MoeRedirecionavel::novo(12, 6, 2, 48, &dev).unwrap();
|
| 1101 |
-
let norma_grads = |vetorizada: bool| -> (f64, f64) {
|
| 1102 |
-
let s = if vetorizada {
|
| 1103 |
-
moe.forward(&x, Papel::Codificador).unwrap()
|
| 1104 |
-
} else {
|
| 1105 |
-
moe.fwd(&x, Papel::Codificador).unwrap()
|
| 1106 |
-
};
|
| 1107 |
-
let perda = s.saida.sqr().unwrap().sum_all().unwrap();
|
| 1108 |
-
let grads = perda.backward().unwrap();
|
| 1109 |
-
let mut n_roteador = 0.0f64;
|
| 1110 |
-
for v in moe.roteador.vars() {
|
| 1111 |
-
if let Some(g) = grads.get(&v) {
|
| 1112 |
-
n_roteador += g.sqr().unwrap().sum_all().unwrap()
|
| 1113 |
-
.to_scalar::<f32>().unwrap() as f64;
|
| 1114 |
-
}
|
| 1115 |
-
}
|
| 1116 |
-
let mut n_experts = 0.0f64;
|
| 1117 |
-
for e in &moe.especialistas {
|
| 1118 |
-
for v in e.vars() {
|
| 1119 |
-
if let Some(g) = grads.get(&v) {
|
| 1120 |
-
n_experts += g.sqr().unwrap().sum_all().unwrap()
|
| 1121 |
-
.to_scalar::<f32>().unwrap() as f64;
|
| 1122 |
-
}
|
| 1123 |
-
}
|
| 1124 |
-
}
|
| 1125 |
-
(n_roteador, n_experts)
|
| 1126 |
-
};
|
| 1127 |
-
// (a) o bug v080-1 é REAL: na fwd, roteador e especialistas têm
|
| 1128 |
-
// gradiente ZERO (grafo seccionado) — documentado, não corrigido na
|
| 1129 |
-
// fwd (referência de valores preservada por decisão de projeto)
|
| 1130 |
-
let (r_ref, e_ref) = norma_grads(false);
|
| 1131 |
-
assert!(
|
| 1132 |
-
r_ref == 0.0 && e_ref == 0.0,
|
| 1133 |
-
"M-R7: bug v080-1 documentado — fwd secciona o grafo (roteador {r_ref}, experts {e_ref})"
|
| 1134 |
-
);
|
| 1135 |
-
// (b) a correção: na vetorizada, TODOS recebem gradiente
|
| 1136 |
-
let (r_vet, e_vet) = norma_grads(true);
|
| 1137 |
-
assert!(
|
| 1138 |
-
r_vet > 0.0,
|
| 1139 |
-
"M-R7: roteador recebe gradiente na vetorizada ({r_vet})"
|
| 1140 |
-
);
|
| 1141 |
-
assert!(
|
| 1142 |
-
e_vet > 0.0,
|
| 1143 |
-
"M-R7: especialistas recebem gradiente na vetorizada ({e_vet})"
|
| 1144 |
-
);
|
| 1145 |
-
}
|
| 1146 |
-
|
| 1147 |
-
/// **M-R8 (v0.8.0):** fim-a-fim — com a vetorizada, o treino REALMENTE
|
| 1148 |
-
/// atualiza os pesos dos especialistas (regressão do bug v080-1: antes
|
| 1149 |
-
/// só a atenção própria treinava no braço MoE).
|
| 1150 |
-
#[test]
|
| 1151 |
-
fn mr8_treino_atualiza_especialistas() {
|
| 1152 |
-
let dev = Device::Cpu;
|
| 1153 |
-
let x = dados_sinteticos(48, 12, 17);
|
| 1154 |
-
let moe = MoeRedirecionavel::novo(12, 6, 2, 48, &dev).unwrap();
|
| 1155 |
-
// snapshot com dados PRÓPRIOS (to_vec1) — detach() compartilha o
|
| 1156 |
-
// storage e o set() do otimizador sobrescreveria a "cópia"
|
| 1157 |
-
let snapshot: Vec<Vec<f32>> = moe
|
| 1158 |
-
.especialistas
|
| 1159 |
-
.iter()
|
| 1160 |
-
.map(|e| {
|
| 1161 |
-
e.vars()[0].as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap()
|
| 1162 |
-
})
|
| 1163 |
-
.collect();
|
| 1164 |
-
// treino curto com gradiente fluindo
|
| 1165 |
-
let vars = moe.vars();
|
| 1166 |
-
let mut opt = crate::treino::AdamWManual::novo(vars.clone(), 3e-3, 0.0);
|
| 1167 |
-
for _ in 0..5 {
|
| 1168 |
-
let s = moe.forward(&x, Papel::Codificador).unwrap();
|
| 1169 |
-
let perda = s.saida.sqr().unwrap().sum_all().unwrap();
|
| 1170 |
-
let grads = crate::treino::grads_achatados(&perda, &vars).unwrap();
|
| 1171 |
-
opt.passo(grads);
|
| 1172 |
-
}
|
| 1173 |
-
let mut delta_max = 0.0f64;
|
| 1174 |
-
for (e, antes) in moe.especialistas.iter().zip(snapshot.iter()) {
|
| 1175 |
-
let agora = e.vars()[0].as_tensor().flatten_all().unwrap()
|
| 1176 |
-
.to_vec1::<f32>().unwrap();
|
| 1177 |
-
let dif = agora.iter().zip(antes.iter()).map(|(a, b)| (a - b).abs()).fold(0f32, f32::max);
|
| 1178 |
-
delta_max = delta_max.max(dif as f64);
|
| 1179 |
-
}
|
| 1180 |
-
let _ = &vars;
|
| 1181 |
-
assert!(
|
| 1182 |
-
delta_max > 0.0,
|
| 1183 |
-
"M-R8: especialistas devem mudar no treino (delta_max={delta_max})"
|
| 1184 |
-
);
|
| 1185 |
-
}
|
| 1186 |
-
}
|
| 1187 |
-
|
| 1188 |
-
#[cfg(test)]
|
| 1189 |
-
mod teste_som_moe_integrado {
|
| 1190 |
-
use super::*;
|
| 1191 |
-
|
| 1192 |
-
#[test]
|
| 1193 |
-
fn debug_som_com_moe_caminho() {
|
| 1194 |
-
let dev = Device::Cpu;
|
| 1195 |
-
let dados_v: Vec<Vec<f64>> = (0..32).map(|i| (0..16).map(|j| (i * 7 + j * 3) as f64 / 100.0).collect()).collect();
|
| 1196 |
-
let (n, d) = (dados_v.len(), 16);
|
| 1197 |
-
let t = Tensor::from_vec(dados_v.into_iter().flatten().collect::<Vec<f64>>(), (n, d), &dev).unwrap()
|
| 1198 |
-
.to_dtype(candle_core::DType::F32).unwrap();
|
| 1199 |
-
let cfg = crate::som4d::CfgSom4D { nx: 2, ny: 2, nz: 2, nt: 2, ..Default::default() };
|
| 1200 |
-
let som = crate::som4d::treinar_som4d(&t, &cfg, &dev).unwrap();
|
| 1201 |
-
let centroides = som.prototipos.clone();
|
| 1202 |
-
let r = refinar_som_com_moe(&t, ¢roides, 30, 1).unwrap();
|
| 1203 |
-
println!("qe {} -> {} (wf) | uniforme {}", r.qe_antes, r.qe_depois, r.qe_alocacao_uniforme);
|
| 1204 |
-
assert!(r.qe_depois < r.qe_antes, "corretor MoE reduz o QE");
|
| 1205 |
-
assert!(r.mse_decodificador < r.qe_antes.powi(2), "redirect decodifica");
|
| 1206 |
-
}
|
| 1207 |
-
}
|
|
|
|
| 1 |
+
//! `moe_redirecionavel::nucleo` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
+
use super::*;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
// ---------------------------------------------------------------------------
|
| 8 |
// MoE redirecionável
|
|
|
|
| 429 |
}
|
| 430 |
c
|
| 431 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/moe_redirecionavel/teste_integrado.rs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `moe_redirecionavel::teste_integrado` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
#[cfg(test)]
|
| 8 |
+
mod teste_som_moe_integrado {
|
| 9 |
+
use super::*;
|
| 10 |
+
|
| 11 |
+
#[test]
|
| 12 |
+
fn debug_som_com_moe_caminho() {
|
| 13 |
+
let dev = Device::Cpu;
|
| 14 |
+
let dados_v: Vec<Vec<f64>> = (0..32).map(|i| (0..16).map(|j| (i * 7 + j * 3) as f64 / 100.0).collect()).collect();
|
| 15 |
+
let (n, d) = (dados_v.len(), 16);
|
| 16 |
+
let t = Tensor::from_vec(dados_v.into_iter().flatten().collect::<Vec<f64>>(), (n, d), &dev).unwrap()
|
| 17 |
+
.to_dtype(candle_core::DType::F32).unwrap();
|
| 18 |
+
let cfg = crate::som4d::CfgSom4D { nx: 2, ny: 2, nz: 2, nt: 2, ..Default::default() };
|
| 19 |
+
let som = crate::som4d::treinar_som4d(&t, &cfg, &dev).unwrap();
|
| 20 |
+
let centroides = som.prototipos.clone();
|
| 21 |
+
let r = refinar_som_com_moe(&t, ¢roides, 30, 1).unwrap();
|
| 22 |
+
println!("qe {} -> {} (wf) | uniforme {}", r.qe_antes, r.qe_depois, r.qe_alocacao_uniforme);
|
| 23 |
+
assert!(r.qe_depois < r.qe_antes, "corretor MoE reduz o QE");
|
| 24 |
+
assert!(r.mse_decodificador < r.qe_antes.powi(2), "redirect decodifica");
|
| 25 |
+
}
|
| 26 |
+
}
|
src/moe_redirecionavel/testes.rs
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `moe_redirecionavel::testes` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
#[cfg(test)]
|
| 8 |
+
#[allow(deprecated)] // testes usam o oráculo fwd (bug v080-1) de propósito
|
| 9 |
+
mod testes {
|
| 10 |
+
use super::*;
|
| 11 |
+
|
| 12 |
+
fn dados_sinteticos(n: usize, d: usize, semente: u64) -> Tensor {
|
| 13 |
+
let mut rng = crate::rng::RngDeterministico::novo(semente);
|
| 14 |
+
let v: Vec<f32> = (0..n * d).map(|_| rng.normal(0.0, 1.0) as f32).collect();
|
| 15 |
+
Tensor::from_vec(v, (n, d), &Device::Cpu).unwrap()
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
#[test]
|
| 19 |
+
fn mr1_invariancia_sob_redirecionamento() {
|
| 20 |
+
let dev = Device::Cpu;
|
| 21 |
+
let x = dados_sinteticos(64, 16, 3);
|
| 22 |
+
let mut moe = MoeRedirecionavel::novo(16, 6, 2, 64, &dev).unwrap();
|
| 23 |
+
let s1 = moe.fwd(&x, Papel::Codificador).unwrap();
|
| 24 |
+
let perda_antes = s1.perda_aux.to_scalar::<f32>().unwrap() as f64;
|
| 25 |
+
let carga_antes: usize = s1.carga.iter().sum();
|
| 26 |
+
moe.redirecionar_todos();
|
| 27 |
+
// após trocar TODOS os papéis, a passada Decodificador usa os
|
| 28 |
+
// especialistas que eram do codificador — a perda auxiliar do
|
| 29 |
+
// SUBCONJUNTO é comparável (mesma distribuição de roteamento)
|
| 30 |
+
let s2 = moe.fwd(&x, Papel::Decodificador).unwrap();
|
| 31 |
+
let perda_depois = s2.perda_aux.to_scalar::<f32>().unwrap() as f64;
|
| 32 |
+
let carga_depois: usize = s2.carga.iter().sum();
|
| 33 |
+
assert_eq!(carga_antes, carga_depois, "capacidade total conservada (M-R1)");
|
| 34 |
+
assert!(
|
| 35 |
+
(perda_antes - perda_depois).abs() < 2.0 + 1e-6,
|
| 36 |
+
"perda auxiliar da família conservada: {perda_antes:.4} vs {perda_depois:.4}"
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
#[test]
|
| 41 |
+
fn mr2_neutralidade_no_nascimento() {
|
| 42 |
+
let dev = Device::Cpu;
|
| 43 |
+
let x = dados_sinteticos(32, 8, 5);
|
| 44 |
+
let moe = MoeRedirecionavel::novo(8, 4, 2, 32, &dev).unwrap();
|
| 45 |
+
let g = moe.atencao.gate.as_tensor().to_scalar::<f32>().unwrap();
|
| 46 |
+
assert!(g == 0.0, "gate nasce zero");
|
| 47 |
+
let y = moe.atencao.fwd(&x).unwrap();
|
| 48 |
+
let dif = (&y - &x).unwrap().sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap();
|
| 49 |
+
assert!(dif == 0.0, "y = x bit-a-bit no nascimento (dif={dif})");
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
#[test]
|
| 53 |
+
fn mr3_paralelismo_e_determinismo() {
|
| 54 |
+
let dev = Device::Cpu;
|
| 55 |
+
let x = dados_sinteticos(96, 24, 7);
|
| 56 |
+
let moe = MoeRedirecionavel::novo(24, 8, 2, 96, &dev).unwrap();
|
| 57 |
+
let (_enc, _dec, t_par, t_seq, speedup) = moe.fwd_ambos_par(&x).unwrap();
|
| 58 |
+
// determinismo já garantido por assert interno; speedup registrado
|
| 59 |
+
assert!(t_par > 0.0 && t_seq > 0.0);
|
| 60 |
+
let _ = speedup;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
#[test]
|
| 64 |
+
fn mr5_water_filling_minimiza_cota() {
|
| 65 |
+
// cota Σ q_i²/c_i: water-filling ≤ uniforme (Cauchy–Schwarz)
|
| 66 |
+
let qes = vec![0.2, 0.5, 1.0, 0.3];
|
| 67 |
+
let c_wf = alocar_por_qe(&qes, 8);
|
| 68 |
+
let c_un = vec![2usize; 4];
|
| 69 |
+
let f = |c: &[usize]| qes.iter().zip(c).map(|(q, ci)| q * q / *ci as f64).sum::<f64>();
|
| 70 |
+
assert!(
|
| 71 |
+
f(&c_wf) <= f(&c_un) + 1e-9,
|
| 72 |
+
"water-filling {c_wf:?} (cota {:.4}) deve ≤ uniforme (cota {:.4})",
|
| 73 |
+
f(&c_wf), f(&c_un)
|
| 74 |
+
);
|
| 75 |
+
assert_eq!(c_wf.iter().sum::<usize>(), 8);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
#[test]
|
| 79 |
+
fn jepa_moe_aprende() {
|
| 80 |
+
let x = dados_sinteticos(64, 12, 11);
|
| 81 |
+
let r = treinar_jepa_com_moe(&x, "moe", 60, 0.5, 42).unwrap();
|
| 82 |
+
assert!(r.perda_pred_final < r.perda_pred_inicial, "perda preditiva cai");
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/// **M-R6 (v0.8.0):** equivalência de VALORES entre `fwd` e
|
| 86 |
+
/// `forward` — mesma seleção (auxiliar compartilhado), mesma
|
| 87 |
+
/// saída, mesma perda auxiliar, mesma carga/descartados.
|
| 88 |
+
#[test]
|
| 89 |
+
fn mr6_equivalencia_de_valores() {
|
| 90 |
+
let dev = Device::Cpu;
|
| 91 |
+
for (n, d, e, k, cap, seed) in [
|
| 92 |
+
(64usize, 16usize, 6usize, 2usize, 64usize, 3u64),
|
| 93 |
+
(96, 24, 8, 2, 96, 5),
|
| 94 |
+
(48, 12, 4, 1, 24, 9), // capacidade apertada: força descartados
|
| 95 |
+
] {
|
| 96 |
+
let x = dados_sinteticos(n, d, seed);
|
| 97 |
+
let moe = MoeRedirecionavel::novo(d, e, k, cap, &dev).unwrap();
|
| 98 |
+
let s_ref = moe.fwd(&x, Papel::Codificador).unwrap();
|
| 99 |
+
let s_vet = moe.forward(&x, Papel::Codificador).unwrap();
|
| 100 |
+
assert_eq!(s_ref.carga, s_vet.carga, "M-R6: mesma carga (n={n})");
|
| 101 |
+
assert_eq!(s_ref.usados, s_vet.usados, "M-R6: mesmos usados");
|
| 102 |
+
assert_eq!(s_ref.descartados, s_vet.descartados, "M-R6: mesmos descartados");
|
| 103 |
+
let dif = (&s_ref.saida - &s_vet.saida).unwrap()
|
| 104 |
+
.abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
|
| 105 |
+
assert!(dif < 1e-4, "M-R6: max|Δsaída| = {dif} (n={n}, cap={cap})");
|
| 106 |
+
let pa = s_ref.perda_aux.to_scalar::<f32>().unwrap();
|
| 107 |
+
let pb = s_vet.perda_aux.to_scalar::<f32>().unwrap();
|
| 108 |
+
assert!(
|
| 109 |
+
(pa - pb).abs() < 1e-4,
|
| 110 |
+
"M-R6: perda auxiliar {pa:.6} vs {pb:.6} (n={n})"
|
| 111 |
+
);
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
/// **M-R7 (v0.8.0):** restauração do fluxo de gradiente — bug v080-1.
|
| 116 |
+
/// Com a `fwd` (referência de valores) os especialistas e o roteador
|
| 117 |
+
/// NÃO recebem gradiente (grafo seccionado por to_vec2/from_vec); com
|
| 118 |
+
/// a `forward` TODOS recebem (index_add/index_select vivos).
|
| 119 |
+
#[test]
|
| 120 |
+
fn mr7_restauracao_do_gradiente() {
|
| 121 |
+
let dev = Device::Cpu;
|
| 122 |
+
let x = dados_sinteticos(48, 12, 13);
|
| 123 |
+
let moe = MoeRedirecionavel::novo(12, 6, 2, 48, &dev).unwrap();
|
| 124 |
+
let norma_grads = |vetorizada: bool| -> (f64, f64) {
|
| 125 |
+
let s = if vetorizada {
|
| 126 |
+
moe.forward(&x, Papel::Codificador).unwrap()
|
| 127 |
+
} else {
|
| 128 |
+
moe.fwd(&x, Papel::Codificador).unwrap()
|
| 129 |
+
};
|
| 130 |
+
let perda = s.saida.sqr().unwrap().sum_all().unwrap();
|
| 131 |
+
let grads = perda.backward().unwrap();
|
| 132 |
+
let mut n_roteador = 0.0f64;
|
| 133 |
+
for v in moe.roteador.vars() {
|
| 134 |
+
if let Some(g) = grads.get(&v) {
|
| 135 |
+
n_roteador += g.sqr().unwrap().sum_all().unwrap()
|
| 136 |
+
.to_scalar::<f32>().unwrap() as f64;
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
let mut n_experts = 0.0f64;
|
| 140 |
+
for e in &moe.especialistas {
|
| 141 |
+
for v in e.vars() {
|
| 142 |
+
if let Some(g) = grads.get(&v) {
|
| 143 |
+
n_experts += g.sqr().unwrap().sum_all().unwrap()
|
| 144 |
+
.to_scalar::<f32>().unwrap() as f64;
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
(n_roteador, n_experts)
|
| 149 |
+
};
|
| 150 |
+
// (a) o bug v080-1 é REAL: na fwd, roteador e especialistas têm
|
| 151 |
+
// gradiente ZERO (grafo seccionado) — documentado, não corrigido na
|
| 152 |
+
// fwd (referência de valores preservada por decisão de projeto)
|
| 153 |
+
let (r_ref, e_ref) = norma_grads(false);
|
| 154 |
+
assert!(
|
| 155 |
+
r_ref == 0.0 && e_ref == 0.0,
|
| 156 |
+
"M-R7: bug v080-1 documentado — fwd secciona o grafo (roteador {r_ref}, experts {e_ref})"
|
| 157 |
+
);
|
| 158 |
+
// (b) a correção: na vetorizada, TODOS recebem gradiente
|
| 159 |
+
let (r_vet, e_vet) = norma_grads(true);
|
| 160 |
+
assert!(
|
| 161 |
+
r_vet > 0.0,
|
| 162 |
+
"M-R7: roteador recebe gradiente na vetorizada ({r_vet})"
|
| 163 |
+
);
|
| 164 |
+
assert!(
|
| 165 |
+
e_vet > 0.0,
|
| 166 |
+
"M-R7: especialistas recebem gradiente na vetorizada ({e_vet})"
|
| 167 |
+
);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/// **M-R8 (v0.8.0):** fim-a-fim — com a vetorizada, o treino REALMENTE
|
| 171 |
+
/// atualiza os pesos dos especialistas (regressão do bug v080-1: antes
|
| 172 |
+
/// só a atenção própria treinava no braço MoE).
|
| 173 |
+
#[test]
|
| 174 |
+
fn mr8_treino_atualiza_especialistas() {
|
| 175 |
+
let dev = Device::Cpu;
|
| 176 |
+
let x = dados_sinteticos(48, 12, 17);
|
| 177 |
+
let moe = MoeRedirecionavel::novo(12, 6, 2, 48, &dev).unwrap();
|
| 178 |
+
// snapshot com dados PRÓPRIOS (to_vec1) — detach() compartilha o
|
| 179 |
+
// storage e o set() do otimizador sobrescreveria a "cópia"
|
| 180 |
+
let snapshot: Vec<Vec<f32>> = moe
|
| 181 |
+
.especialistas
|
| 182 |
+
.iter()
|
| 183 |
+
.map(|e| {
|
| 184 |
+
e.vars()[0].as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap()
|
| 185 |
+
})
|
| 186 |
+
.collect();
|
| 187 |
+
// treino curto com gradiente fluindo
|
| 188 |
+
let vars = moe.vars();
|
| 189 |
+
let mut opt = crate::treino::AdamWManual::novo(vars.clone(), 3e-3, 0.0);
|
| 190 |
+
for _ in 0..5 {
|
| 191 |
+
let s = moe.forward(&x, Papel::Codificador).unwrap();
|
| 192 |
+
let perda = s.saida.sqr().unwrap().sum_all().unwrap();
|
| 193 |
+
let grads = crate::treino::grads_achatados(&perda, &vars).unwrap();
|
| 194 |
+
opt.passo(grads);
|
| 195 |
+
}
|
| 196 |
+
let mut delta_max = 0.0f64;
|
| 197 |
+
for (e, antes) in moe.especialistas.iter().zip(snapshot.iter()) {
|
| 198 |
+
let agora = e.vars()[0].as_tensor().flatten_all().unwrap()
|
| 199 |
+
.to_vec1::<f32>().unwrap();
|
| 200 |
+
let dif = agora.iter().zip(antes.iter()).map(|(a, b)| (a - b).abs()).fold(0f32, f32::max);
|
| 201 |
+
delta_max = delta_max.max(dif as f64);
|
| 202 |
+
}
|
| 203 |
+
let _ = &vars;
|
| 204 |
+
assert!(
|
| 205 |
+
delta_max > 0.0,
|
| 206 |
+
"M-R8: especialistas devem mudar no treino (delta_max={delta_max})"
|
| 207 |
+
);
|
| 208 |
+
}
|
| 209 |
+
}
|
src/moe_redirecionavel/treino.rs
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `moe_redirecionavel::treino` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
// ---------------------------------------------------------------------------
|
| 8 |
+
// Aplicação JEPA — codificador/preditor redirecionáveis com MoE
|
| 9 |
+
// ---------------------------------------------------------------------------
|
| 10 |
+
|
| 11 |
+
pub struct RelatorioJepaMoe {
|
| 12 |
+
pub modo: &'static str,
|
| 13 |
+
pub perda_pred_inicial: f64,
|
| 14 |
+
pub perda_pred_final: f64,
|
| 15 |
+
pub perda_vc_final: f64,
|
| 16 |
+
pub std_emb: f64,
|
| 17 |
+
pub carga_max: usize,
|
| 18 |
+
pub descartados: usize,
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
/// Treino JEPA compacto com três braços (ablação honesta):
|
| 22 |
+
/// - `"pequena"`: codificador/preditor MLP simples (baseline);
|
| 23 |
+
/// - `"densa"`: MLP com o TOTAL de parâmetros dos especialistas (capacidade
|
| 24 |
+
/// equiparada, SEM roteamento — isola o ganho do roteamento);
|
| 25 |
+
/// - `"moe"`: `MoeRedirecionavel` nos dois caminhos (Codificador no
|
| 26 |
+
/// codificador, Decodificador no preditor) com atenção própria.
|
| 27 |
+
pub fn treinar_jepa_com_moe(
|
| 28 |
+
patches: &Tensor,
|
| 29 |
+
modo: &'static str,
|
| 30 |
+
passos: usize,
|
| 31 |
+
frac_mascara: f64,
|
| 32 |
+
semente: u64,
|
| 33 |
+
) -> Result<RelatorioJepaMoe> {
|
| 34 |
+
let dev = patches.device();
|
| 35 |
+
let n = patches.dim(0)?;
|
| 36 |
+
let d = patches.dim(1)?;
|
| 37 |
+
let mut rng = RngDeterministico::novo(semente);
|
| 38 |
+
// alvo = patches originais (detach); contexto = patches mascarados
|
| 39 |
+
let alvo = patches.detach();
|
| 40 |
+
let contexto = {
|
| 41 |
+
let mut v = patches.flatten_all()?.to_vec1::<f32>()?;
|
| 42 |
+
for x in v.iter_mut() {
|
| 43 |
+
if rng.uniforme() < frac_mascara {
|
| 44 |
+
*x = 0.0;
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
Tensor::from_vec(v, (n, d), &dev)?
|
| 48 |
+
};
|
| 49 |
+
let e = 4usize; // especialistas por papel no braço moe
|
| 50 |
+
let n_top = 2;
|
| 51 |
+
let capacidade = n;
|
| 52 |
+
|
| 53 |
+
// braços
|
| 54 |
+
enum Braco {
|
| 55 |
+
Pequena(MlpJepa, MlpJepa),
|
| 56 |
+
Densa(MlpJepa, MlpJepa),
|
| 57 |
+
Moe(MoeRedirecionavel),
|
| 58 |
+
}
|
| 59 |
+
let braco = match modo {
|
| 60 |
+
"pequena" => Braco::Pequena(
|
| 61 |
+
MlpJepa::novo(d, 2 * d, d, &dev)?,
|
| 62 |
+
MlpJepa::novo(d, 2 * d, d, &dev)?,
|
| 63 |
+
),
|
| 64 |
+
"densa" => {
|
| 65 |
+
// capacidade equiparada: oculta = soma das ocultas dos especialistas
|
| 66 |
+
let d_oculta = 2 * d * e;
|
| 67 |
+
Braco::Densa(
|
| 68 |
+
MlpJepa::novo(d, d_oculta, d, &dev)?,
|
| 69 |
+
MlpJepa::novo(d, d_oculta, d, &dev)?,
|
| 70 |
+
)
|
| 71 |
+
}
|
| 72 |
+
_ => Braco::Moe(MoeRedirecionavel::novo(d, 2 * e, n_top, capacidade, &dev)?),
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
let perda_de = |b: &Braco, x: &Tensor| -> Result<(Tensor, Tensor, usize)> {
|
| 76 |
+
match b {
|
| 77 |
+
Braco::Pequena(e1, p1) => {
|
| 78 |
+
let z = e1.fwd(x)?;
|
| 79 |
+
let yh = p1.fwd(&z)?;
|
| 80 |
+
let dif = (&yh - &alvo)?;
|
| 81 |
+
Ok(((dif.sqr()?.sum_all()? / (n as f64 * d as f64))?,
|
| 82 |
+
z.sqr()?.mean_all()?, 0))
|
| 83 |
+
}
|
| 84 |
+
Braco::Densa(e1, p1) => {
|
| 85 |
+
let z = e1.fwd(x)?;
|
| 86 |
+
let yh = p1.fwd(&z)?;
|
| 87 |
+
let dif = (&yh - &alvo)?;
|
| 88 |
+
Ok(((dif.sqr()?.sum_all()? / (n as f64 * d as f64))?,
|
| 89 |
+
z.sqr()?.mean_all()?, 0))
|
| 90 |
+
}
|
| 91 |
+
Braco::Moe(m) => {
|
| 92 |
+
// v0.9.0: forward (ex-fwd_vetorizado) — gradiente flui aos
|
| 93 |
+
// especialistas e ao roteador (a fwd seccionava o grafo)
|
| 94 |
+
let s_enc = m.forward(x, Papel::Codificador)?;
|
| 95 |
+
let s_dec = m.forward(&s_enc.saida, Papel::Decodificador)?;
|
| 96 |
+
let dif = (&s_dec.saida - &alvo)?;
|
| 97 |
+
let lp = (dif.sqr()?.sum_all()? / (n as f64 * d as f64))?;
|
| 98 |
+
// perda total = preditiva + aux balanceamento (ambos papéis)
|
| 99 |
+
let lt = (lp.affine(1.0, 0.0)? + s_enc.perda_aux.affine(m.lambda_aux, 0.0)?
|
| 100 |
+
+ s_dec.perda_aux.affine(m.lambda_aux, 0.0)?)?;
|
| 101 |
+
let carga_max = s_enc.carga.iter().copied().fold(0, usize::max)
|
| 102 |
+
.max(s_dec.carga.iter().copied().fold(0, usize::max));
|
| 103 |
+
Ok((lt, lp, carga_max))
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
let (mut opt, vars): (AdamWManual, Vec<Var>) = match &braco {
|
| 109 |
+
Braco::Pequena(a, b) => {
|
| 110 |
+
let v = [a.vars(), b.vars()].concat();
|
| 111 |
+
(AdamWManual::novo(v.clone(), 3e-3, 0.0), v)
|
| 112 |
+
}
|
| 113 |
+
Braco::Densa(a, b) => {
|
| 114 |
+
let v = [a.vars(), b.vars()].concat();
|
| 115 |
+
(AdamWManual::novo(v.clone(), 3e-3, 0.0), v)
|
| 116 |
+
}
|
| 117 |
+
Braco::Moe(m) => (AdamWManual::novo(m.vars(), 3e-3, 0.0), m.vars()),
|
| 118 |
+
};
|
| 119 |
+
// atenção própria: gate nasce 0; sobe 1 passo em 8 (aquecimento honesto
|
| 120 |
+
// do mecanismo — T-P5 análogo: neutralidade no nascimento, gradiente flui)
|
| 121 |
+
if let Braco::Moe(m) = &braco {
|
| 122 |
+
let g0 = m.atencao.gate.as_tensor().to_scalar::<f32>()? as f64;
|
| 123 |
+
let _ = g0;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
let (l0, _, _) = perda_de(&braco, &contexto)?;
|
| 127 |
+
let perda_inicial = l0.to_scalar::<f32>()? as f64;
|
| 128 |
+
let mut perda_final = perda_inicial;
|
| 129 |
+
let mut vc_final = 0.0;
|
| 130 |
+
let mut carga_max = 0usize;
|
| 131 |
+
let mut descartados = 0usize;
|
| 132 |
+
let mut std_emb = 0.0;
|
| 133 |
+
for passo in 0..passos {
|
| 134 |
+
let (lt, lp, cm) = perda_de(&braco, &contexto)?;
|
| 135 |
+
if let Braco::Moe(m) = &braco {
|
| 136 |
+
// abre o gate gradualmente (0 → 1 em 25% dos passos)
|
| 137 |
+
let alvo_g = ((passo as f64 / (passos as f64 / 4.0).max(1.0)).min(1.0)) as f32;
|
| 138 |
+
let g_atual = m.atencao.gate.as_tensor().to_scalar::<f32>()? as f64;
|
| 139 |
+
if (alvo_g as f64 - g_atual).abs() > 1e-9 {
|
| 140 |
+
m.atencao.gate.set(&Tensor::new(alvo_g, &dev)?)?;
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
let grads = crate::treino::grads_achatados(<, &vars)?;
|
| 144 |
+
AdamWManual::clipar(&mut grads.clone(), 1.0);
|
| 145 |
+
let mut g2 = grads;
|
| 146 |
+
AdamWManual::clipar(&mut g2, 5.0);
|
| 147 |
+
opt.passo(g2);
|
| 148 |
+
perda_final = lp.to_scalar::<f32>()? as f64;
|
| 149 |
+
carga_max = cm;
|
| 150 |
+
// VC / std do embedding (para o relatório)
|
| 151 |
+
let (_, vc, _) = perda_de(&braco, &contexto)?;
|
| 152 |
+
vc_final = vc.to_scalar::<f32>()? as f64;
|
| 153 |
+
std_emb = {
|
| 154 |
+
let z = match &braco {
|
| 155 |
+
Braco::Pequena(a, _) => a.fwd(&contexto)?,
|
| 156 |
+
Braco::Densa(a, _) => a.fwd(&contexto)?,
|
| 157 |
+
Braco::Moe(m) => m.forward(&contexto, Papel::Codificador)?.saida,
|
| 158 |
+
};
|
| 159 |
+
z.flatten_all()?.to_vec1::<f32>()?.iter().map(|x| *x as f64).collect::<Vec<f64>>()
|
| 160 |
+
.chunks(n).map(|c| {
|
| 161 |
+
let m = c.iter().sum::<f64>() / c.len() as f64;
|
| 162 |
+
(c.iter().map(|x| (x - m).powi(2)).sum::<f64>() / c.len() as f64).sqrt()
|
| 163 |
+
}).sum::<f64>() / n as f64
|
| 164 |
+
};
|
| 165 |
+
}
|
| 166 |
+
if let Braco::Moe(m) = &braco {
|
| 167 |
+
let s = m.forward(&contexto, Papel::Codificador)?;
|
| 168 |
+
descartados = s.descartados;
|
| 169 |
+
}
|
| 170 |
+
Ok(RelatorioJepaMoe {
|
| 171 |
+
modo,
|
| 172 |
+
perda_pred_inicial: perda_inicial,
|
| 173 |
+
perda_pred_final: perda_final,
|
| 174 |
+
perda_vc_final: vc_final,
|
| 175 |
+
std_emb,
|
| 176 |
+
carga_max,
|
| 177 |
+
descartados,
|
| 178 |
+
})
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// ---------------------------------------------------------------------------
|
| 182 |
+
// Aplicação SOM — refinamento residual com alocação por QE (M-R5)
|
| 183 |
+
// ---------------------------------------------------------------------------
|
| 184 |
+
|
| 185 |
+
pub struct RelatorioSomMoe {
|
| 186 |
+
pub qe_antes: f64,
|
| 187 |
+
pub qe_depois: f64,
|
| 188 |
+
pub qe_alocacao_uniforme: f64,
|
| 189 |
+
pub carga_por_celula: Vec<usize>,
|
| 190 |
+
pub tempo_treino_s: f64,
|
| 191 |
+
/// qualidade da DECODIFICAÇÃO após redirecionamento (mse w_bmu → x).
|
| 192 |
+
pub mse_decodificador: f64,
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
/// Refina um SOM 4D treinado com um MoE redirecionável:
|
| 196 |
+
/// 1. aloca especialistas por célula ∝ QE (water-filling — M-R5);
|
| 197 |
+
/// 2. treina o papel Codificador como corretor residual
|
| 198 |
+
/// `x̂ = w_bmu(x) + E(x)`;
|
| 199 |
+
/// 3. mede QE′; repete com alocação uniforme (ablação do water-filling);
|
| 200 |
+
/// 4. REDIRECIONA os especialistas para Decodificador e treina
|
| 201 |
+
/// `x̂ = E(w_bmu)` (reconstrução a partir do código) — mede o MSE.
|
| 202 |
+
pub fn refinar_som_com_moe(
|
| 203 |
+
dados: &Tensor,
|
| 204 |
+
centroides: &[Vec<f64>],
|
| 205 |
+
passos: usize,
|
| 206 |
+
semente: u64,
|
| 207 |
+
) -> Result<RelatorioSomMoe> {
|
| 208 |
+
use std::time::Instant;
|
| 209 |
+
let t0 = Instant::now();
|
| 210 |
+
let dev = dados.device();
|
| 211 |
+
let n = dados.dim(0)?;
|
| 212 |
+
let d = dados.dim(1)?;
|
| 213 |
+
let m = centroides.len();
|
| 214 |
+
// BMU + QE por célula (tolera F32/F64)
|
| 215 |
+
let dados_v: Vec<Vec<f64>> = match dados.to_vec2::<f64>() {
|
| 216 |
+
Ok(v) => v,
|
| 217 |
+
Err(_) => dados
|
| 218 |
+
.to_vec2::<f32>()?
|
| 219 |
+
.iter()
|
| 220 |
+
.map(|l| l.iter().map(|x| *x as f64).collect())
|
| 221 |
+
.collect(),
|
| 222 |
+
};
|
| 223 |
+
let bmu: Vec<usize> = dados_v
|
| 224 |
+
.iter()
|
| 225 |
+
.map(|x| {
|
| 226 |
+
centroides
|
| 227 |
+
.iter()
|
| 228 |
+
.enumerate()
|
| 229 |
+
.min_by(|(i, a), (j, b)| {
|
| 230 |
+
let da: f64 = a.iter().zip(x).map(|(w, v)| (w - v).powi(2)).sum();
|
| 231 |
+
let db: f64 = b.iter().zip(x).map(|(w, v)| (w - v).powi(2)).sum();
|
| 232 |
+
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal).then(i.cmp(j))
|
| 233 |
+
})
|
| 234 |
+
.map(|(i, _)| i)
|
| 235 |
+
.unwrap_or(0)
|
| 236 |
+
})
|
| 237 |
+
.collect();
|
| 238 |
+
let mut qe_cel = vec![0f64; m];
|
| 239 |
+
let mut cont_cel = vec![0usize; m];
|
| 240 |
+
for (i, b) in bmu.iter().enumerate() {
|
| 241 |
+
let dist: f64 = centroides[*b].iter().zip(&dados_v[i]).map(|(w, v)| (w - v).powi(2)).sum();
|
| 242 |
+
qe_cel[*b] += dist.sqrt();
|
| 243 |
+
cont_cel[*b] += 1;
|
| 244 |
+
}
|
| 245 |
+
for i in 0..m {
|
| 246 |
+
if cont_cel[i] > 0 {
|
| 247 |
+
qe_cel[i] /= cont_cel[i] as f64;
|
| 248 |
+
}
|
| 249 |
+
}
|
| 250 |
+
let qe_antes: f64 = dados_v
|
| 251 |
+
.iter()
|
| 252 |
+
.enumerate()
|
| 253 |
+
.map(|(i, b)| {
|
| 254 |
+
let w = ¢roides[bmu[i]];
|
| 255 |
+
(w.iter().zip(b).map(|(a, v)| (a - v).powi(2)).sum::<f64>()).sqrt()
|
| 256 |
+
})
|
| 257 |
+
.sum::<f64>()
|
| 258 |
+
/ n as f64;
|
| 259 |
+
|
| 260 |
+
// ---------- braço water-filling ----------
|
| 261 |
+
// capacidade = m células + 8 especialistas EXTRA distribuídos ∝ QE
|
| 262 |
+
// (com C < m o water-filling degenera em uniforme — mínimo 1 por célula)
|
| 263 |
+
let capacidade_total = m + 8;
|
| 264 |
+
let aloca_wf = alocar_por_qe(&qe_cel, capacidade_total);
|
| 265 |
+
let qe_wf = treinar_corretor(dados, centroides, &bmu, &aloca_wf, passos, semente, &dev)?;
|
| 266 |
+
// ---------- braço uniforme (ablação: MESMO total e MESMA semente —
|
| 267 |
+
// única diferença é a alocação; round-robin soma exatamente C) ----------
|
| 268 |
+
let aloca_un = {
|
| 269 |
+
let mut a = vec![1usize; m];
|
| 270 |
+
let mut i = 0usize;
|
| 271 |
+
while a.iter().sum::<usize>() < capacidade_total {
|
| 272 |
+
a[i % m] += 1;
|
| 273 |
+
i += 1;
|
| 274 |
+
}
|
| 275 |
+
a
|
| 276 |
+
};
|
| 277 |
+
let qe_un = treinar_corretor(dados, centroides, &bmu, &aloca_un, passos, semente, &dev)?;
|
| 278 |
+
|
| 279 |
+
// ---------- redirecionamento → decodificador ----------
|
| 280 |
+
// mesma pool de especialistas, agora aprendendo w_bmu → x
|
| 281 |
+
let mse_dec = treinar_decodificador(dados, centroides, &bmu, &aloca_wf, passos, semente + 2, &dev)?;
|
| 282 |
+
|
| 283 |
+
Ok(RelatorioSomMoe {
|
| 284 |
+
qe_antes,
|
| 285 |
+
qe_depois: qe_wf,
|
| 286 |
+
qe_alocacao_uniforme: qe_un,
|
| 287 |
+
carga_por_celula: aloca_wf,
|
| 288 |
+
tempo_treino_s: t0.elapsed().as_secs_f64(),
|
| 289 |
+
mse_decodificador: mse_dec,
|
| 290 |
+
})
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
/// Corretor residual: x̂ = w_bmu + E_{célula}(x). Treina com AdamW.
|
| 294 |
+
fn treinar_corretor(
|
| 295 |
+
dados: &Tensor,
|
| 296 |
+
centroides: &[Vec<f64>],
|
| 297 |
+
bmu: &[usize],
|
| 298 |
+
alocacao: &[usize],
|
| 299 |
+
passos: usize,
|
| 300 |
+
semente: u64,
|
| 301 |
+
dev: &Device,
|
| 302 |
+
) -> Result<f64> {
|
| 303 |
+
let n = dados.dim(0)?;
|
| 304 |
+
let d = dados.dim(1)?;
|
| 305 |
+
// um "especialista" = MlpJepa por CÉLULA (capacidade alocada = nº de
|
| 306 |
+
// especialistas extra treinados nessa célula, somados em ensemble)
|
| 307 |
+
let n_esp_tot: usize = alocacao.iter().sum();
|
| 308 |
+
let mut ensembles: Vec<Vec<MlpJepa>> = Vec::with_capacity(centroides.len());
|
| 309 |
+
for c in alocacao {
|
| 310 |
+
let mut v = Vec::new();
|
| 311 |
+
for _ in 0..(*c).max(1) {
|
| 312 |
+
v.push(MlpJepa::novo(d, 4 * d, d, dev)?);
|
| 313 |
+
}
|
| 314 |
+
ensembles.push(v);
|
| 315 |
+
}
|
| 316 |
+
let mut vars: Vec<Var> = Vec::new();
|
| 317 |
+
for ens in &ensembles {
|
| 318 |
+
for e in ens {
|
| 319 |
+
vars.extend(e.vars());
|
| 320 |
+
}
|
| 321 |
+
}
|
| 322 |
+
let mut opt = AdamWManual::novo(vars.clone(), 3e-3, 0.0);
|
| 323 |
+
// tensores de apoio
|
| 324 |
+
let w_bmu = {
|
| 325 |
+
let v: Vec<f32> = (0..n).flat_map(|i| centroides[bmu[i]].iter().map(|x| *x as f32).collect::<Vec<f32>>()).collect();
|
| 326 |
+
Tensor::from_vec(v, (n, d), dev)?
|
| 327 |
+
};
|
| 328 |
+
let alvo_res = (dados - &w_bmu)?;
|
| 329 |
+
for _ in 0..passos {
|
| 330 |
+
let mut soma = Tensor::zeros((), candle_core::DType::F32, dev)?;
|
| 331 |
+
for (i, ens) in ensembles.iter().enumerate() {
|
| 332 |
+
// tokens da célula i
|
| 333 |
+
let idx: Vec<u32> = (0..n).filter(|&k| bmu[k] == i).map(|k| k as u32).collect();
|
| 334 |
+
if idx.is_empty() {
|
| 335 |
+
continue;
|
| 336 |
+
}
|
| 337 |
+
let m_idx = idx.len();
|
| 338 |
+
let it = Tensor::from_vec(idx, (m_idx,), dev)?;
|
| 339 |
+
let x_c = dados.index_select(&it, 0)?;
|
| 340 |
+
let r_c = alvo_res.index_select(&it, 0)?;
|
| 341 |
+
// ensemble: média das saídas
|
| 342 |
+
let mut y = Tensor::zeros((m_idx, d), candle_core::DType::F32, dev)?;
|
| 343 |
+
for e in ens {
|
| 344 |
+
y = (y.affine(1.0, 0.0)? + e.fwd(&x_c)?.affine(1.0, 0.0)?)?;
|
| 345 |
+
}
|
| 346 |
+
let k = ens.len() as f64;
|
| 347 |
+
let yh = y.affine(1.0 / k, 0.0)?;
|
| 348 |
+
let dif = (&yh - &r_c)?;
|
| 349 |
+
soma = (soma.affine(1.0, 0.0)? + dif.sqr()?.sum_all()?.affine(1.0 / (n as f64 * d as f64), 0.0)?)?;
|
| 350 |
+
}
|
| 351 |
+
let _ = n_esp_tot;
|
| 352 |
+
let grads = crate::treino::grads_achatados(&soma, &vars)?;
|
| 353 |
+
let mut g = grads;
|
| 354 |
+
AdamWManual::clipar(&mut g, 5.0);
|
| 355 |
+
opt.passo(g);
|
| 356 |
+
}
|
| 357 |
+
// QE refinado
|
| 358 |
+
let mut qe = 0f64;
|
| 359 |
+
for (i, ens) in ensembles.iter().enumerate() {
|
| 360 |
+
let idx: Vec<u32> = (0..n).filter(|&k| bmu[k] == i).map(|k| k as u32).collect();
|
| 361 |
+
if idx.is_empty() {
|
| 362 |
+
continue;
|
| 363 |
+
}
|
| 364 |
+
let m_idx = idx.len();
|
| 365 |
+
let it = Tensor::from_vec(idx, (m_idx,), dev)?;
|
| 366 |
+
let x_c = dados.index_select(&it, 0)?;
|
| 367 |
+
let mut y = Tensor::zeros((m_idx, d), candle_core::DType::F32, dev)?;
|
| 368 |
+
for e in ens {
|
| 369 |
+
y = (y.affine(1.0, 0.0)? + e.fwd(&x_c)?.affine(1.0, 0.0)?)?;
|
| 370 |
+
}
|
| 371 |
+
let k = ens.len() as f64;
|
| 372 |
+
let yh = y.affine(1.0 / k, 0.0)?;
|
| 373 |
+
let xhat = (w_bmu.index_select(&it, 0)? + &yh)?;
|
| 374 |
+
let dif = (&x_c - &xhat)?;
|
| 375 |
+
qe += dif.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
|
| 376 |
+
}
|
| 377 |
+
Ok((qe / (n as f64 * d as f64)).sqrt() * (d as f64).sqrt()) // QE médio por ponto (norma L2)
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
/// Decodificador (pós-redirecionamento): x̂ = E(w_bmu) — reconstrução
|
| 381 |
+
/// a partir apenas do código (centróide). Mede o MSE final.
|
| 382 |
+
fn treinar_decodificador(
|
| 383 |
+
dados: &Tensor,
|
| 384 |
+
centroides: &[Vec<f64>],
|
| 385 |
+
bmu: &[usize],
|
| 386 |
+
alocacao: &[usize],
|
| 387 |
+
passos: usize,
|
| 388 |
+
semente: u64,
|
| 389 |
+
dev: &Device,
|
| 390 |
+
) -> Result<f64> {
|
| 391 |
+
let _ = semente;
|
| 392 |
+
let n = dados.dim(0)?;
|
| 393 |
+
let d = dados.dim(1)?;
|
| 394 |
+
let mut ensembles: Vec<Vec<MlpJepa>> = Vec::with_capacity(centroides.len());
|
| 395 |
+
for c in alocacao {
|
| 396 |
+
let mut v = Vec::new();
|
| 397 |
+
for _ in 0..(*c).max(1) {
|
| 398 |
+
v.push(MlpJepa::novo(d, 4 * d, d, dev)?);
|
| 399 |
+
}
|
| 400 |
+
ensembles.push(v);
|
| 401 |
+
}
|
| 402 |
+
let mut vars: Vec<Var> = Vec::new();
|
| 403 |
+
for ens in &ensembles {
|
| 404 |
+
for e in ens {
|
| 405 |
+
vars.extend(e.vars());
|
| 406 |
+
}
|
| 407 |
+
}
|
| 408 |
+
let mut opt = AdamWManual::novo(vars.clone(), 3e-3, 0.0);
|
| 409 |
+
let w_bmu = {
|
| 410 |
+
let v: Vec<f32> = (0..n).flat_map(|i| centroides[bmu[i]].iter().map(|x| *x as f32).collect::<Vec<f32>>()).collect();
|
| 411 |
+
Tensor::from_vec(v, (n, d), dev)?
|
| 412 |
+
};
|
| 413 |
+
for _ in 0..passos {
|
| 414 |
+
let mut soma = Tensor::zeros((), candle_core::DType::F32, dev)?;
|
| 415 |
+
for (i, ens) in ensembles.iter().enumerate() {
|
| 416 |
+
let idx: Vec<u32> = (0..n).filter(|&k| bmu[k] == i).map(|k| k as u32).collect();
|
| 417 |
+
if idx.is_empty() {
|
| 418 |
+
continue;
|
| 419 |
+
}
|
| 420 |
+
let m_idx = idx.len();
|
| 421 |
+
let it = Tensor::from_vec(idx, (m_idx,), dev)?;
|
| 422 |
+
let w_c = w_bmu.index_select(&it, 0)?;
|
| 423 |
+
let x_c = dados.index_select(&it, 0)?;
|
| 424 |
+
let mut y = Tensor::zeros((m_idx, d), candle_core::DType::F32, dev)?;
|
| 425 |
+
for e in ens {
|
| 426 |
+
y = (y.affine(1.0, 0.0)? + e.fwd(&w_c)?.affine(1.0, 0.0)?)?;
|
| 427 |
+
}
|
| 428 |
+
let k = ens.len() as f64;
|
| 429 |
+
let yh = y.affine(1.0 / k, 0.0)?;
|
| 430 |
+
let dif = (&yh - &x_c)?;
|
| 431 |
+
soma = (soma.affine(1.0, 0.0)? + dif.sqr()?.sum_all()?.affine(1.0 / (n as f64 * d as f64), 0.0)?)?;
|
| 432 |
+
}
|
| 433 |
+
let grads = crate::treino::grads_achatados(&soma, &vars)?;
|
| 434 |
+
let mut g = grads;
|
| 435 |
+
AdamWManual::clipar(&mut g, 5.0);
|
| 436 |
+
opt.passo(g);
|
| 437 |
+
}
|
| 438 |
+
// MSE final do decodificador
|
| 439 |
+
let mut mse = 0f64;
|
| 440 |
+
for (i, ens) in ensembles.iter().enumerate() {
|
| 441 |
+
let idx: Vec<u32> = (0..n).filter(|&k| bmu[k] == i).map(|k| k as u32).collect();
|
| 442 |
+
if idx.is_empty() {
|
| 443 |
+
continue;
|
| 444 |
+
}
|
| 445 |
+
let m_idx = idx.len();
|
| 446 |
+
let it = Tensor::from_vec(idx, (m_idx,), dev)?;
|
| 447 |
+
let w_c = w_bmu.index_select(&it, 0)?;
|
| 448 |
+
let x_c = dados.index_select(&it, 0)?;
|
| 449 |
+
let mut y = Tensor::zeros((m_idx, d), candle_core::DType::F32, dev)?;
|
| 450 |
+
for e in ens {
|
| 451 |
+
y = (y.affine(1.0, 0.0)? + e.fwd(&w_c)?.affine(1.0, 0.0)?)?;
|
| 452 |
+
}
|
| 453 |
+
let k = ens.len() as f64;
|
| 454 |
+
let yh = y.affine(1.0 / k, 0.0)?;
|
| 455 |
+
let dif = (&yh - &x_c)?;
|
| 456 |
+
mse += dif.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
|
| 457 |
+
}
|
| 458 |
+
Ok(mse / (n as f64 * d as f64))
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
pub fn versao() -> &'static str {
|
| 462 |
+
"moe-redirecionavel-0.6.0"
|
| 463 |
+
}
|
src/pensamento/combinador.rs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `pensamento::combinador` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
// ---------------------------------------------------------------------------
|
| 8 |
+
// Combinador algébrico (T-P3 — projeção ridge, Teo V-1' portado)
|
| 9 |
+
// ---------------------------------------------------------------------------
|
| 10 |
+
|
| 11 |
+
/// Projeção ridge: `r* = (WᵀW + εI)^{−1} Wᵀ r_alvo` — minimizador de
|
| 12 |
+
/// `‖W r − r_alvo‖² + ε‖r‖²` (o montador algébrico do "re-encaixe de peças").
|
| 13 |
+
///
|
| 14 |
+
/// **Prova (T-P3):** a função é quadrática estritamente convexa (ε > 0);
|
| 15 |
+
/// o gradiente anula-se exatamente em `r*` acima; o resíduo mínimo é
|
| 16 |
+
/// `‖(I − W(WᵀW+εI)^{−1}Wᵀ) r_alvo‖` — análogo direto do Teo V-1'.
|
| 17 |
+
pub fn projetar_ridge(w: &Tensor, r_alvo: &Tensor, eps: f64) -> Result<Tensor> {
|
| 18 |
+
// w: (m, n), r_alvo: (m, d) ⇒ r*: (n, d)
|
| 19 |
+
let n = w.dim(1)?;
|
| 20 |
+
let dev = w.device();
|
| 21 |
+
let wt = w.transpose(0, 1)?;
|
| 22 |
+
let wtw = wt.matmul(w)?;
|
| 23 |
+
let ident = Tensor::eye(n, candle_core::DType::F32, &dev)?.affine(eps, 0.0)?;
|
| 24 |
+
let m = (&wtw + &ident)?;
|
| 25 |
+
let rhs = wt.matmul(r_alvo)?;
|
| 26 |
+
resolver_simetrica(&m, &rhs)
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/// Resolve `M x = B` para M simétrica (n, n) por eliminação de Gauss com
|
| 30 |
+
/// pivoteamento parcial (Cholesky não assumido — M pode ser só PSD+εI).
|
| 31 |
+
pub fn resolver_simetrica(m: &Tensor, b: &Tensor) -> Result<Tensor> {
|
| 32 |
+
let n = m.dim(0)?;
|
| 33 |
+
let d = b.dim(1)?;
|
| 34 |
+
let dev = m.device();
|
| 35 |
+
let mut a = m.to_vec2::<f32>()?;
|
| 36 |
+
let mut rhs = b.to_vec2::<f32>()?;
|
| 37 |
+
for col in 0..n {
|
| 38 |
+
// pivoteamento parcial
|
| 39 |
+
let (mut pivo, mut linha_p) = (a[col][col].abs(), col);
|
| 40 |
+
for r in col + 1..n {
|
| 41 |
+
if a[r][col].abs() > pivo {
|
| 42 |
+
pivo = a[r][col].abs();
|
| 43 |
+
linha_p = r;
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
if pivo < 1e-12 {
|
| 47 |
+
candle_core::bail!("matriz singular (pivo ~ 0 na coluna {col})");
|
| 48 |
+
}
|
| 49 |
+
a.swap(col, linha_p);
|
| 50 |
+
rhs.swap(col, linha_p);
|
| 51 |
+
for r in col + 1..n {
|
| 52 |
+
let f = a[r][col] / a[col][col];
|
| 53 |
+
for c in col..n {
|
| 54 |
+
a[r][c] -= f * a[col][c];
|
| 55 |
+
}
|
| 56 |
+
for c in 0..d {
|
| 57 |
+
rhs[r][c] -= f * rhs[col][c];
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
let mut x = vec![vec![0f32; d]; n];
|
| 62 |
+
for r in (0..n).rev() {
|
| 63 |
+
for c in 0..d {
|
| 64 |
+
let mut s = rhs[r][c];
|
| 65 |
+
for k in r + 1..n {
|
| 66 |
+
s -= a[r][k] * x[k][c];
|
| 67 |
+
}
|
| 68 |
+
x[r][c] = s / a[r][r];
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
let flat: Vec<f32> = x.into_iter().flatten().collect();
|
| 72 |
+
Ok(Tensor::from_vec(flat, (n, d), &dev)?)
|
| 73 |
+
}
|
src/pensamento/mod.rs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Tecnologia Pensamento — módulos: motor, combinador ridge,
|
| 2 |
+
//! quebra-cabeça pós-treino (divisão SRP v0.10.0 — r-35 §2).
|
| 3 |
+
|
| 4 |
+
//! Tecnologia **Pensamento** v0.5.0 — N-redes neurais flexíveis paralelizadas
|
| 5 |
+
//! integradas ao NCM, conectadas ao Mecanismo de Atenção, com raciocínio
|
| 6 |
+
//! temporal-estratégico (item d = a + b + c).
|
| 7 |
+
//!
|
| 8 |
+
//! **O que é (motivação do usuário):** Pensamento é semelhante ao
|
| 9 |
+
//! pós-treinamento — N-redes paralelas de tentativas cooperativas para
|
| 10 |
+
//! resolver o problema do item c (requisição do usuário) quando a
|
| 11 |
+
//! informação tem POUCA CONFIANÇA para gerar novo conhecimento (fora dos
|
| 12 |
+
//! padrões comuns), a partir de:
|
| 13 |
+
//! - **a)** conhecimento aprendido durante o treinamento;
|
| 14 |
+
//! - **b)** estado de inferência corrente;
|
| 15 |
+
//! - **c)** a requisição (prompt) do usuário;
|
| 16 |
+
//! e da ativação de **difusores** (exploração estocástica) e
|
| 17 |
+
//! **combinadores/montadores** (re-encaixe de peças por projeção algébrica).
|
| 18 |
+
//!
|
| 19 |
+
//! **Ciclo d (raciocínio temporal-estratégico):**
|
| 20 |
+
//! `planejamento → construção de etapas e distribuição/desmembramento de
|
| 21 |
+
//! competências → volta ao planejamento OU envio da resposta ao usuário`.
|
| 22 |
+
//!
|
| 23 |
+
//! **Provas (completas em `docs/memorias/r-21-provas-pensamento-v050.md`):**
|
| 24 |
+
//! - **T-P1 (gatilho mensurável e causal):** o gatilho
|
| 25 |
+
//! `1{conf(p) < τ_c}` com `conf = 1 − H(p)/log K` é função apenas da
|
| 26 |
+
//! distribuição preditiva CORRENTE — não transporta informação do futuro,
|
| 27 |
+
//! preservando a causalidade (mesma estrutura do gate G1 do NCM).
|
| 28 |
+
//! - **T-P2 (tentativas paralelas — auto-consistência):** com N tentativas
|
| 29 |
+
//! independentes, cada uma correta com prob. `p > 1/2`, a votação por
|
| 30 |
+
//! maioria tem `P(erro) ≤ exp(−2N(p−1/2)²)` (Hoeffding); com seleção
|
| 31 |
+
//! best-of-N por verificador exato, `P(sucesso) ≥ 1−(1−p)^N`.
|
| 32 |
+
//! - **T-P3 (combinador ótimo):** o montador por mínimos quadrados com
|
| 33 |
+
//! regularização ridge resolve `r* = (WᵀW+εI)^{−1}Wᵀr_alvo` — o resíduo é
|
| 34 |
+
//! o MÍNIMO possível na norma (pseudo-inversa, Teo V-1' do outro agente).
|
| 35 |
+
//! - **T-P4 (terminação e soundness do planejador):** o potencial
|
| 36 |
+
//! `Φ(k) = B − k` decresce estritamente a cada iteração do ciclo d ⇒ o
|
| 37 |
+
//! loop termina em ≤ B iterações (indução bem-fundada); a resposta só é
|
| 38 |
+
//! emitida com confiança ≥ τ_c OU com orçamento esgotado (declaração
|
| 39 |
+
//! honesta de queda de confiança na telemetria).
|
| 40 |
+
//! - **T-P5 (neutralidade no nascimento na atenção):** com o gate de
|
| 41 |
+
//! atenção das tentativas inicializado em ZERO, a saída da atenção é
|
| 42 |
+
//! bit-a-bit a da atenção base; a perturbação é limitada por
|
| 43 |
+
//! `‖Δy‖ ≤ L·|g|·‖W_k z‖` (Lipschitz com constante L do softmax).
|
| 44 |
+
//! - **T-P6 (composição d = a + b + c):** o condicionamento de cada
|
| 45 |
+
//! tentativa é `cond = [a; b; c]` — a estrutura de independência
|
| 46 |
+
//! condicional `p(resposta | a, b, c)` fatoriza pelo estado conjunto; as
|
| 47 |
+
//! tentativas só veem o PASSADO (causalidade preservada).
|
| 48 |
+
|
| 49 |
+
use candle_core::{Device, Result, Tensor, Var};
|
| 50 |
+
use crate::camadas::Linear;
|
| 51 |
+
use crate::jepa::MlpJepa;
|
| 52 |
+
use crate::rng::RngDeterministico;
|
| 53 |
+
use std::collections::HashMap;
|
| 54 |
+
|
| 55 |
+
// ---- divisão SRP v0.10.0 (r-35 §2): submódulos + re-exports ----
|
| 56 |
+
#[cfg(test)]
|
| 57 |
+
mod testes;
|
| 58 |
+
mod combinador;
|
| 59 |
+
mod motor;
|
| 60 |
+
mod quebra_cabeca;
|
| 61 |
+
|
| 62 |
+
pub use combinador::*;
|
| 63 |
+
pub use motor::*;
|
| 64 |
+
pub use quebra_cabeca::*;
|
| 65 |
+
|
| 66 |
+
// ---- itens mantidos no próprio mod.rs ----
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
pub fn versao() -> &'static str {
|
| 70 |
+
"pensamento-0.7.0"
|
| 71 |
+
}
|
src/{pensamento.rs → pensamento/motor.rs}
RENAMED
|
@@ -1,53 +1,8 @@
|
|
| 1 |
-
//!
|
| 2 |
-
//!
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
//! **O que é (motivação do usuário):** Pensamento é semelhante ao
|
| 6 |
-
//! pós-treinamento — N-redes paralelas de tentativas cooperativas para
|
| 7 |
-
//! resolver o problema do item c (requisição do usuário) quando a
|
| 8 |
-
//! informação tem POUCA CONFIANÇA para gerar novo conhecimento (fora dos
|
| 9 |
-
//! padrões comuns), a partir de:
|
| 10 |
-
//! - **a)** conhecimento aprendido durante o treinamento;
|
| 11 |
-
//! - **b)** estado de inferência corrente;
|
| 12 |
-
//! - **c)** a requisição (prompt) do usuário;
|
| 13 |
-
//! e da ativação de **difusores** (exploração estocástica) e
|
| 14 |
-
//! **combinadores/montadores** (re-encaixe de peças por projeção algébrica).
|
| 15 |
-
//!
|
| 16 |
-
//! **Ciclo d (raciocínio temporal-estratégico):**
|
| 17 |
-
//! `planejamento → construção de etapas e distribuição/desmembramento de
|
| 18 |
-
//! competências → volta ao planejamento OU envio da resposta ao usuário`.
|
| 19 |
-
//!
|
| 20 |
-
//! **Provas (completas em `docs/memorias/r-21-provas-pensamento-v050.md`):**
|
| 21 |
-
//! - **T-P1 (gatilho mensurável e causal):** o gatilho
|
| 22 |
-
//! `1{conf(p) < τ_c}` com `conf = 1 − H(p)/log K` é função apenas da
|
| 23 |
-
//! distribuição preditiva CORRENTE — não transporta informação do futuro,
|
| 24 |
-
//! preservando a causalidade (mesma estrutura do gate G1 do NCM).
|
| 25 |
-
//! - **T-P2 (tentativas paralelas — auto-consistência):** com N tentativas
|
| 26 |
-
//! independentes, cada uma correta com prob. `p > 1/2`, a votação por
|
| 27 |
-
//! maioria tem `P(erro) ≤ exp(−2N(p−1/2)²)` (Hoeffding); com seleção
|
| 28 |
-
//! best-of-N por verificador exato, `P(sucesso) ≥ 1−(1−p)^N`.
|
| 29 |
-
//! - **T-P3 (combinador ótimo):** o montador por mínimos quadrados com
|
| 30 |
-
//! regularização ridge resolve `r* = (WᵀW+εI)^{−1}Wᵀr_alvo` — o resíduo é
|
| 31 |
-
//! o MÍNIMO possível na norma (pseudo-inversa, Teo V-1' do outro agente).
|
| 32 |
-
//! - **T-P4 (terminação e soundness do planejador):** o potencial
|
| 33 |
-
//! `Φ(k) = B − k` decresce estritamente a cada iteração do ciclo d ⇒ o
|
| 34 |
-
//! loop termina em ≤ B iterações (indução bem-fundada); a resposta só é
|
| 35 |
-
//! emitida com confiança ≥ τ_c OU com orçamento esgotado (declaração
|
| 36 |
-
//! honesta de queda de confiança na telemetria).
|
| 37 |
-
//! - **T-P5 (neutralidade no nascimento na atenção):** com o gate de
|
| 38 |
-
//! atenção das tentativas inicializado em ZERO, a saída da atenção é
|
| 39 |
-
//! bit-a-bit a da atenção base; a perturbação é limitada por
|
| 40 |
-
//! `‖Δy‖ ≤ L·|g|·‖W_k z‖` (Lipschitz com constante L do softmax).
|
| 41 |
-
//! - **T-P6 (composição d = a + b + c):** o condicionamento de cada
|
| 42 |
-
//! tentativa é `cond = [a; b; c]` — a estrutura de independência
|
| 43 |
-
//! condicional `p(resposta | a, b, c)` fatoriza pelo estado conjunto; as
|
| 44 |
-
//! tentativas só veem o PASSADO (causalidade preservada).
|
| 45 |
|
| 46 |
-
use candle_core::{Device, Result, Tensor, Var};
|
| 47 |
-
use crate::camadas::Linear;
|
| 48 |
-
use crate::jepa::MlpJepa;
|
| 49 |
-
use crate::rng::RngDeterministico;
|
| 50 |
-
use std::collections::HashMap;
|
| 51 |
|
| 52 |
// ---------------------------------------------------------------------------
|
| 53 |
// Confiança e gatilho (T-P1)
|
|
@@ -592,601 +547,3 @@ impl MotorPensamento {
|
|
| 592 |
}
|
| 593 |
}
|
| 594 |
}
|
| 595 |
-
|
| 596 |
-
// ---------------------------------------------------------------------------
|
| 597 |
-
// Combinador algébrico (T-P3 — projeção ridge, Teo V-1' portado)
|
| 598 |
-
// ---------------------------------------------------------------------------
|
| 599 |
-
|
| 600 |
-
/// Projeção ridge: `r* = (WᵀW + εI)^{−1} Wᵀ r_alvo` — minimizador de
|
| 601 |
-
/// `‖W r − r_alvo‖² + ε‖r‖²` (o montador algébrico do "re-encaixe de peças").
|
| 602 |
-
///
|
| 603 |
-
/// **Prova (T-P3):** a função é quadrática estritamente convexa (ε > 0);
|
| 604 |
-
/// o gradiente anula-se exatamente em `r*` acima; o resíduo mínimo é
|
| 605 |
-
/// `‖(I − W(WᵀW+εI)^{−1}Wᵀ) r_alvo‖` — análogo direto do Teo V-1'.
|
| 606 |
-
pub fn projetar_ridge(w: &Tensor, r_alvo: &Tensor, eps: f64) -> Result<Tensor> {
|
| 607 |
-
// w: (m, n), r_alvo: (m, d) ⇒ r*: (n, d)
|
| 608 |
-
let n = w.dim(1)?;
|
| 609 |
-
let dev = w.device();
|
| 610 |
-
let wt = w.transpose(0, 1)?;
|
| 611 |
-
let wtw = wt.matmul(w)?;
|
| 612 |
-
let ident = Tensor::eye(n, candle_core::DType::F32, &dev)?.affine(eps, 0.0)?;
|
| 613 |
-
let m = (&wtw + &ident)?;
|
| 614 |
-
let rhs = wt.matmul(r_alvo)?;
|
| 615 |
-
resolver_simetrica(&m, &rhs)
|
| 616 |
-
}
|
| 617 |
-
|
| 618 |
-
/// Resolve `M x = B` para M simétrica (n, n) por eliminação de Gauss com
|
| 619 |
-
/// pivoteamento parcial (Cholesky não assumido — M pode ser só PSD+εI).
|
| 620 |
-
pub fn resolver_simetrica(m: &Tensor, b: &Tensor) -> Result<Tensor> {
|
| 621 |
-
let n = m.dim(0)?;
|
| 622 |
-
let d = b.dim(1)?;
|
| 623 |
-
let dev = m.device();
|
| 624 |
-
let mut a = m.to_vec2::<f32>()?;
|
| 625 |
-
let mut rhs = b.to_vec2::<f32>()?;
|
| 626 |
-
for col in 0..n {
|
| 627 |
-
// pivoteamento parcial
|
| 628 |
-
let (mut pivo, mut linha_p) = (a[col][col].abs(), col);
|
| 629 |
-
for r in col + 1..n {
|
| 630 |
-
if a[r][col].abs() > pivo {
|
| 631 |
-
pivo = a[r][col].abs();
|
| 632 |
-
linha_p = r;
|
| 633 |
-
}
|
| 634 |
-
}
|
| 635 |
-
if pivo < 1e-12 {
|
| 636 |
-
candle_core::bail!("matriz singular (pivo ~ 0 na coluna {col})");
|
| 637 |
-
}
|
| 638 |
-
a.swap(col, linha_p);
|
| 639 |
-
rhs.swap(col, linha_p);
|
| 640 |
-
for r in col + 1..n {
|
| 641 |
-
let f = a[r][col] / a[col][col];
|
| 642 |
-
for c in col..n {
|
| 643 |
-
a[r][c] -= f * a[col][c];
|
| 644 |
-
}
|
| 645 |
-
for c in 0..d {
|
| 646 |
-
rhs[r][c] -= f * rhs[col][c];
|
| 647 |
-
}
|
| 648 |
-
}
|
| 649 |
-
}
|
| 650 |
-
let mut x = vec![vec![0f32; d]; n];
|
| 651 |
-
for r in (0..n).rev() {
|
| 652 |
-
for c in 0..d {
|
| 653 |
-
let mut s = rhs[r][c];
|
| 654 |
-
for k in r + 1..n {
|
| 655 |
-
s -= a[r][k] * x[k][c];
|
| 656 |
-
}
|
| 657 |
-
x[r][c] = s / a[r][r];
|
| 658 |
-
}
|
| 659 |
-
}
|
| 660 |
-
let flat: Vec<f32> = x.into_iter().flatten().collect();
|
| 661 |
-
Ok(Tensor::from_vec(flat, (n, d), &dev)?)
|
| 662 |
-
}
|
| 663 |
-
|
| 664 |
-
pub fn versao() -> &'static str {
|
| 665 |
-
"pensamento-0.7.0"
|
| 666 |
-
}
|
| 667 |
-
|
| 668 |
-
// ---------------------------------------------------------------------------
|
| 669 |
-
// QUEBRA-CABEÇA pós-treinamento (v0.7.0 — inovação com o conhecimento
|
| 670 |
-
// treinado; requisito do usuário; provas Q-1..Q-6 em r-31)
|
| 671 |
-
// ---------------------------------------------------------------------------
|
| 672 |
-
|
| 673 |
-
/// Configuração do quebra-cabeça (r-28 §4.3).
|
| 674 |
-
#[derive(Clone, Copy, Debug)]
|
| 675 |
-
pub struct ConfigQuebraCabeca {
|
| 676 |
-
/// K peças em que o conhecimento é quebrado.
|
| 677 |
-
pub n_pecas: usize,
|
| 678 |
-
/// orçamento B de rodadas (terminação Q-4).
|
| 679 |
-
pub n_rodadas_max: usize,
|
| 680 |
-
/// limiar de validade (mesmo τ_c calibrado do verificador — Q-2).
|
| 681 |
-
pub tau_c: f64,
|
| 682 |
-
/// teto do ajuste por componente na árvore (regulação).
|
| 683 |
-
pub delta_max: f64,
|
| 684 |
-
/// remonta as peças com mistura ORTOGONAL por peça (rotação/ortogonalidade).
|
| 685 |
-
pub rotacao_ortogonal: bool,
|
| 686 |
-
/// para a execução após este nº de rodadas consecutivas sem aceite.
|
| 687 |
-
pub paciencia: usize,
|
| 688 |
-
}
|
| 689 |
-
|
| 690 |
-
impl Default for ConfigQuebraCabeca {
|
| 691 |
-
fn default() -> Self {
|
| 692 |
-
ConfigQuebraCabeca {
|
| 693 |
-
n_pecas: 4,
|
| 694 |
-
n_rodadas_max: 12,
|
| 695 |
-
tau_c: 0.55,
|
| 696 |
-
delta_max: 0.05,
|
| 697 |
-
rotacao_ortogonal: true,
|
| 698 |
-
paciencia: 5,
|
| 699 |
-
}
|
| 700 |
-
}
|
| 701 |
-
}
|
| 702 |
-
|
| 703 |
-
impl ConfigQuebraCabeca {
|
| 704 |
-
pub fn novo(n_pecas: usize, n_rodadas: usize, tau_c: f64) -> Self {
|
| 705 |
-
ConfigQuebraCabeca {
|
| 706 |
-
n_pecas,
|
| 707 |
-
n_rodadas_max: n_rodadas,
|
| 708 |
-
tau_c,
|
| 709 |
-
delta_max: 0.05,
|
| 710 |
-
rotacao_ortogonal: true,
|
| 711 |
-
paciencia: (n_rodadas / 2).max(2),
|
| 712 |
-
}
|
| 713 |
-
}
|
| 714 |
-
}
|
| 715 |
-
|
| 716 |
-
/// Uma rodada do quebra-cabeça.
|
| 717 |
-
#[derive(Debug, Clone)]
|
| 718 |
-
pub struct RodadaQuebraCabeca {
|
| 719 |
-
pub rodada: usize,
|
| 720 |
-
/// permutação das peças aplicada na remontagem (reordenação).
|
| 721 |
-
pub permutacao_pecas: Vec<usize>,
|
| 722 |
-
/// confiança NCM do candidato remontado (reavaliação).
|
| 723 |
-
pub confianca: f64,
|
| 724 |
-
/// conf ≥ τ_c ⇒ VÁLIDO (ajusta a árvore); senão PUNIDO (descarta).
|
| 725 |
-
pub aceito: bool,
|
| 726 |
-
/// delta aplicado na árvore (0 quando descartado).
|
| 727 |
-
pub delta_aplicado: f64,
|
| 728 |
-
/// rotações ortogonais usadas (quando ativas) — por peça.
|
| 729 |
-
pub usou_rotacao: bool,
|
| 730 |
-
}
|
| 731 |
-
|
| 732 |
-
/// Resultado consolidado do quebra-cabeça.
|
| 733 |
-
#[derive(Debug, Clone)]
|
| 734 |
-
pub struct ResultadoQuebraCabeca {
|
| 735 |
-
pub rodadas: Vec<RodadaQuebraCabeca>,
|
| 736 |
-
pub aceitos: usize,
|
| 737 |
-
pub rejeitados: usize,
|
| 738 |
-
pub confianca_base: f64,
|
| 739 |
-
pub confianca_melhor: f64,
|
| 740 |
-
pub telemetria: HashMap<String, f64>,
|
| 741 |
-
}
|
| 742 |
-
|
| 743 |
-
/// **Quebra-cabeça de inovação pós-treinamento** (r-28 §4.3):
|
| 744 |
-
/// 1. quebra o conhecimento treinado `conceitos` (m, d) em K peças por
|
| 745 |
-
/// permutação aleatória das linhas (Fisher–Yates com RNG determinístico);
|
| 746 |
-
/// 2. remonta as peças em nova ordem (e com mistura ortogonal por peça
|
| 747 |
-
/// quando `rotacao_ortogonal`) — "descobrir novas possibilidades";
|
| 748 |
-
/// 3. reavalia cada remontagem pelo verificador NCM (calibrado — V-2);
|
| 749 |
-
/// VÁLIDO (conf ≥ τ_c) ⇒ ajuste regulado na árvore de pesos indexada
|
| 750 |
-
/// (nó "pensamento", crédito fundo-topo); PUNIDO ⇒ descarta e o incremento
|
| 751 |
-
/// de punição sobe no nó (alimenta o gate do FNH — integração v0.7.0);
|
| 752 |
-
/// 4. repete até o orçamento B ou a paciência esgotar (Q-4).
|
| 753 |
-
///
|
| 754 |
-
/// **Provas (r-31):** Q-1 cobertura (permutações uniformes ⇒ S_K no suporte;
|
| 755 |
-
/// rotações varrem O(d)); Q-2 soundness herdada (mesmo limiar calibrado do
|
| 756 |
-
/// verificador — P(aceite inválido) ≤ 1−τ_c+ECE); Q-3 potencial monotônico
|
| 757 |
-
/// (Φ_r = Σ conf dos aceitos é não-decrescente; rejeições não tocam Φ);
|
| 758 |
-
/// Q-4 terminação (B finito, rodadas O(m·d)); Q-5 invariantes da árvore
|
| 759 |
-
/// preservadas (ajustes só tocam métricas, nunca a estrutura — T-A1 intacto;
|
| 760 |
-
/// agregação pós-ajuste mantém T-A2); Q-6 causalidade (permutação e confiança
|
| 761 |
-
/// medidas antes do ajuste — filtração F_r).
|
| 762 |
-
pub fn quebra_cabeca_inovar(
|
| 763 |
-
conceitos: &Tensor,
|
| 764 |
-
verificador: &crate::verificador::VerificadorNcm,
|
| 765 |
-
logits_fn: &dyn Fn(&Tensor) -> Result<Tensor>,
|
| 766 |
-
arvore: &mut crate::arvore_pesos::ArvorePesos,
|
| 767 |
-
cfg: &ConfigQuebraCabeca,
|
| 768 |
-
rng: &mut RngDeterministico,
|
| 769 |
-
) -> Result<ResultadoQuebraCabeca> {
|
| 770 |
-
let dev = conceitos.device().clone();
|
| 771 |
-
let (m, d) = (conceitos.dims()[0], conceitos.dims()[1]);
|
| 772 |
-
let k = cfg.n_pecas.clamp(1, m);
|
| 773 |
-
let no_pens = arvore.no("pensamento").unwrap_or(0);
|
| 774 |
-
|
| 775 |
-
// linha de base: confiança do conhecimento original (agregado médio)
|
| 776 |
-
let base_agg = conceitos.mean(0)?;
|
| 777 |
-
let conf_base = {
|
| 778 |
-
let lg = logits_fn(&base_agg)?;
|
| 779 |
-
verificador.confianca_1d(&lg)?
|
| 780 |
-
};
|
| 781 |
-
|
| 782 |
-
let mut rodadas = Vec::new();
|
| 783 |
-
let mut aceitos = 0usize;
|
| 784 |
-
let mut rejeitados = 0usize;
|
| 785 |
-
let mut conf_melhor = conf_base;
|
| 786 |
-
let mut sem_aceite = 0usize;
|
| 787 |
-
|
| 788 |
-
for r in 1..=cfg.n_rodadas_max {
|
| 789 |
-
if sem_aceite >= cfg.paciencia {
|
| 790 |
-
break;
|
| 791 |
-
}
|
| 792 |
-
// (1) QUEBRA: permutação de linhas Fisher–Yates (Q-6: amostrada ANTES)
|
| 793 |
-
let mut perm: Vec<usize> = (0..m).collect();
|
| 794 |
-
for i in (1..m).rev() {
|
| 795 |
-
let j = rng.inteiro(i + 1);
|
| 796 |
-
perm.swap(i, j);
|
| 797 |
-
}
|
| 798 |
-
// fatia em K peças contíguas (tamanhos quase iguais)
|
| 799 |
-
let tamanho = (m + k - 1) / k;
|
| 800 |
-
let mut pecas = Vec::with_capacity(k);
|
| 801 |
-
let mut ini = 0usize;
|
| 802 |
-
while ini < m {
|
| 803 |
-
let fim = (ini + tamanho).min(m);
|
| 804 |
-
let idx: Vec<u32> = perm[ini..fim].iter().map(|&x| x as u32).collect();
|
| 805 |
-
let pedaco = conceitos.index_select(&Tensor::from_vec(idx, (fim - ini,), &dev)?, 0)?;
|
| 806 |
-
pecas.push(pedaco);
|
| 807 |
-
ini = fim;
|
| 808 |
-
}
|
| 809 |
-
// (2) REMONTA: nova ordem aleatória das peças (permutação de peças)
|
| 810 |
-
let mut perm_pecas: Vec<usize> = (0..pecas.len()).collect();
|
| 811 |
-
for i in (1..pecas.len()).rev() {
|
| 812 |
-
let j = rng.inteiro(i + 1);
|
| 813 |
-
perm_pecas.swap(i, j);
|
| 814 |
-
}
|
| 815 |
-
let mut remontada = Vec::with_capacity(pecas.len());
|
| 816 |
-
for &pi in &perm_pecas {
|
| 817 |
-
let mut pedaco = pecas[pi].clone();
|
| 818 |
-
if cfg.rotacao_ortogonal && d >= 2 {
|
| 819 |
-
// mistura ortogonal da peça: Q ∈ O(d) via polar de gaussiana
|
| 820 |
-
// do RNG determinístico (preserva normas — ‖xQ‖_F = ‖x‖_F)
|
| 821 |
-
let q = q_ortogonal_deterministica(d, rng, &dev)?;
|
| 822 |
-
pedaco = pedaco.matmul(&q)?;
|
| 823 |
-
}
|
| 824 |
-
remontada.push(pedaco);
|
| 825 |
-
}
|
| 826 |
-
let candidata = Tensor::cat(&remontada, 0)?; // (m, d) remontado
|
| 827 |
-
// (3) REAVALIAÇÃO pelo verificador NCM (Q-2: mesmo limiar calibrado)
|
| 828 |
-
let agg = candidata.mean(0)?;
|
| 829 |
-
let lg = logits_fn(&agg)?;
|
| 830 |
-
let conf = verificador.confianca_1d(&lg)?;
|
| 831 |
-
let aceito = conf >= cfg.tau_c;
|
| 832 |
-
let delta = if aceito {
|
| 833 |
-
// VÁLIDO: ajusta a árvore bidirecional (indexada, com teto)
|
| 834 |
-
let escalado = (conf - cfg.tau_c).min(cfg.delta_max);
|
| 835 |
-
arvore.aplicar_ajuste_indexado(no_pens, "confianca", escalado, cfg.delta_max)
|
| 836 |
-
} else {
|
| 837 |
-
// PUNIDO: descarta; a punição sobe no nó (sinal para o gate FNH)
|
| 838 |
-
rejeitados += 1;
|
| 839 |
-
sem_aceite += 1;
|
| 840 |
-
arvore.aplicar_ajuste_indexado(no_pens, "punicao", cfg.delta_max * 0.5, cfg.delta_max);
|
| 841 |
-
0.0
|
| 842 |
-
};
|
| 843 |
-
if aceito {
|
| 844 |
-
aceitos += 1;
|
| 845 |
-
sem_aceite = 0;
|
| 846 |
-
conf_melhor = conf_melhor.max(conf);
|
| 847 |
-
}
|
| 848 |
-
rodadas.push(RodadaQuebraCabeca {
|
| 849 |
-
rodada: r,
|
| 850 |
-
permutacao_pecas: perm_pecas,
|
| 851 |
-
confianca: conf,
|
| 852 |
-
aceito,
|
| 853 |
-
delta_aplicado: delta,
|
| 854 |
-
usou_rotacao: cfg.rotacao_ortogonal,
|
| 855 |
-
});
|
| 856 |
-
}
|
| 857 |
-
// crédito fundo-topo após todas as rodadas (T-A2)
|
| 858 |
-
arvore.agregar_fundo_topo("confianca");
|
| 859 |
-
arvore.agregar_fundo_topo("punicao");
|
| 860 |
-
|
| 861 |
-
let n_rod = rodadas.len();
|
| 862 |
-
let mut telemetria = HashMap::new();
|
| 863 |
-
telemetria.insert("rodadas".into(), n_rod as f64);
|
| 864 |
-
telemetria.insert("aceitos".into(), aceitos as f64);
|
| 865 |
-
telemetria.insert("rejeitados".into(), rejeitados as f64);
|
| 866 |
-
telemetria.insert("confianca_base".into(), conf_base);
|
| 867 |
-
telemetria.insert("confianca_melhor".into(), conf_melhor);
|
| 868 |
-
telemetria.insert(
|
| 869 |
-
"taxa_aceitacao".into(),
|
| 870 |
-
if n_rod > 0 { aceitos as f64 / n_rod as f64 } else { 0.0 },
|
| 871 |
-
);
|
| 872 |
-
telemetria.insert("rotacao_ativa".into(), if cfg.rotacao_ortogonal { 1.0 } else { 0.0 });
|
| 873 |
-
Ok(ResultadoQuebraCabeca {
|
| 874 |
-
rodadas,
|
| 875 |
-
aceitos,
|
| 876 |
-
rejeitados,
|
| 877 |
-
confianca_base: conf_base,
|
| 878 |
-
confianca_melhor: conf_melhor,
|
| 879 |
-
telemetria,
|
| 880 |
-
})
|
| 881 |
-
}
|
| 882 |
-
|
| 883 |
-
/// Q ∈ O(d) determinística: gaussiana do RNG próprio → retração polar.
|
| 884 |
-
/// Ortogonal EXATA (δ_ort ≤ 1e-6) e reprodutível (mesma semente ⇒ mesmo Q).
|
| 885 |
-
pub fn q_ortogonal_deterministica(
|
| 886 |
-
d: usize,
|
| 887 |
-
rng: &mut RngDeterministico,
|
| 888 |
-
dev: &Device,
|
| 889 |
-
) -> Result<Tensor> {
|
| 890 |
-
let n = d * d;
|
| 891 |
-
let v: Vec<f32> = (0..n).map(|_| rng.normal_padrao() as f32).collect();
|
| 892 |
-
let g = Tensor::from_vec(v, (d, d), dev)?;
|
| 893 |
-
crate::stiefel::retrair_polar_frobenius(&g, 90)
|
| 894 |
-
}
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
#[cfg(test)]
|
| 898 |
-
mod testes {
|
| 899 |
-
use super::*;
|
| 900 |
-
|
| 901 |
-
#[test]
|
| 902 |
-
fn teste_confianca_extremos() {
|
| 903 |
-
// T-P1: Dirac ⇒ conf = 1; uniforme ⇒ conf = 0
|
| 904 |
-
let dev = Device::Cpu;
|
| 905 |
-
let dirac = Tensor::from_vec(vec![100.0f32, 0.0, 0.0, 0.0], (1, 4), &dev).unwrap();
|
| 906 |
-
let uniforme = Tensor::from_vec(vec![0.0f32; 4], (1, 4), &dev).unwrap();
|
| 907 |
-
let c1 = confianca_de_logits(&dirac).unwrap();
|
| 908 |
-
let c0 = confianca_de_logits(&uniforme).unwrap();
|
| 909 |
-
assert!(c1 > 0.999, "conf dirac = {c1}");
|
| 910 |
-
assert!(c0 < 0.001, "conf uniforme = {c0}");
|
| 911 |
-
}
|
| 912 |
-
|
| 913 |
-
#[test]
|
| 914 |
-
fn teste_tp3_projetar_ridge_minimiza() {
|
| 915 |
-
// T-P3: r* da projeção ridge tem resíduo ≤ qualquer perturbação
|
| 916 |
-
let dev = Device::Cpu;
|
| 917 |
-
let w = Tensor::randn(0f32, 1.0, (8, 3), &dev).unwrap();
|
| 918 |
-
let r_alvo = Tensor::randn(0f32, 1.0, (8, 5), &dev).unwrap();
|
| 919 |
-
let r = projetar_ridge(&w, &r_alvo, 1e-6).unwrap();
|
| 920 |
-
let residuo = |rr: &Tensor| {
|
| 921 |
-
let d = (w.matmul(rr).unwrap() - &r_alvo).unwrap();
|
| 922 |
-
d.sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap() as f64
|
| 923 |
-
};
|
| 924 |
-
let f0 = residuo(&r);
|
| 925 |
-
let mut rng = RngDeterministico::novo(1);
|
| 926 |
-
for _ in 0..30 {
|
| 927 |
-
let pert: Vec<f32> = (0..15).map(|_| rng.normal(0.0, 0.1) as f32).collect();
|
| 928 |
-
let rp = (Tensor::from_vec(pert, (3, 5), &dev).unwrap() + &r).unwrap();
|
| 929 |
-
let fp = residuo(&rp);
|
| 930 |
-
assert!(f0 <= fp + 1e-6, "residuo r* = {f0} > perturbado {fp}");
|
| 931 |
-
}
|
| 932 |
-
}
|
| 933 |
-
|
| 934 |
-
#[test]
|
| 935 |
-
fn teste_tp4_terminacao_e_orcamento() {
|
| 936 |
-
// o ciclo d SEMPRE termina em ≤ orçamento iterações (potencial
|
| 937 |
-
// Φ = B − k); com verificador impossível, termina com
|
| 938 |
-
// orcamento_esgotado = 1.
|
| 939 |
-
let dev = Device::Cpu;
|
| 940 |
-
let mut motor = MotorPensamento::novo(6, 4, 3, 0.99, 5, &dev).unwrap();
|
| 941 |
-
let cond = Tensor::randn(0f32, 1.0, (6,), &dev).unwrap();
|
| 942 |
-
let mut rng = RngDeterministico::novo(2);
|
| 943 |
-
let res = motor
|
| 944 |
-
.raciocinar(&cond, &|_| 0.0, &mut rng) // verificador sempre 0
|
| 945 |
-
.unwrap();
|
| 946 |
-
assert!(res.iteracoes <= 5, "iterações = {} > orçamento", res.iteracoes);
|
| 947 |
-
assert_eq!(res.telemetria.get("orcamento_esgotado"), Some(&1.0));
|
| 948 |
-
assert!(res.resposta.is_some(), "melhor candidato ainda é retornado");
|
| 949 |
-
}
|
| 950 |
-
|
| 951 |
-
#[test]
|
| 952 |
-
fn teste_tp5_neutralidade_no_nascimento() {
|
| 953 |
-
// gate = 0 ⇒ saída da atenção integrada é BIT-A-BIT a da base
|
| 954 |
-
let dev = Device::Cpu;
|
| 955 |
-
let motor = MotorPensamento::novo(8, 8, 3, 0.5, 3, &dev).unwrap();
|
| 956 |
-
let q = Tensor::randn(0f32, 1.0, (1, 4, 8), &dev).unwrap();
|
| 957 |
-
let k = Tensor::randn(0f32, 1.0, (1, 6, 8), &dev).unwrap();
|
| 958 |
-
let v = Tensor::randn(0f32, 1.0, (1, 6, 8), &dev).unwrap();
|
| 959 |
-
let cand = Tensor::randn(0f32, 1.0, (3, 8), &dev).unwrap();
|
| 960 |
-
let y_int = motor.atencao_integrada(&q, &k, &v, &cand).unwrap();
|
| 961 |
-
// atenção base pura
|
| 962 |
-
let scores = (q.matmul(&k.transpose(1, 2).unwrap()).unwrap() / (8f64).sqrt()).unwrap();
|
| 963 |
-
let attn = candle_nn::ops::softmax(&scores, candle_core::D::Minus1).unwrap();
|
| 964 |
-
let y_base = attn.matmul(&v).unwrap();
|
| 965 |
-
let dif = (&y_int - &y_base).unwrap().abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
|
| 966 |
-
assert!(dif == 0.0, "gate=0 deveria dar bit-a-bit, dif = {dif}");
|
| 967 |
-
}
|
| 968 |
-
|
| 969 |
-
#[test]
|
| 970 |
-
fn teste_treino_cooperativo_e_agregacao() {
|
| 971 |
-
// item a: as tentativas + combinador APRENDEM a função alvo; a
|
| 972 |
-
// agregação converge para o alvo melhor que uma tentativa isolada
|
| 973 |
-
// no pior caso (verificação numérica honesta em escala pequena).
|
| 974 |
-
let dev = Device::Cpu;
|
| 975 |
-
let mut motor = MotorPensamento::novo(6, 4, 3, 0.5, 3, &dev).unwrap();
|
| 976 |
-
let vars = motor.vars();
|
| 977 |
-
let mut otim = crate::treino::AdamWManual::novo(vars, 5e-3, 0.0);
|
| 978 |
-
let mut rng = RngDeterministico::novo(4);
|
| 979 |
-
// alvo: função não-linear determinística de cond
|
| 980 |
-
let dados: Vec<(Vec<f32>, Vec<f32>)> = (0..256)
|
| 981 |
-
.map(|_| {
|
| 982 |
-
let c: Vec<f32> = (0..6).map(|_| rng.normal(0.0, 1.0) as f32).collect();
|
| 983 |
-
let a: Vec<f32> = (0..4)
|
| 984 |
-
.map(|j| {
|
| 985 |
-
(c[j % 6] * 0.5 + (c[(j + 1) % 6] * c[(j + 2) % 6]) * 0.3 + 0.1) as f32
|
| 986 |
-
})
|
| 987 |
-
.collect();
|
| 988 |
-
(c, a)
|
| 989 |
-
})
|
| 990 |
-
.collect();
|
| 991 |
-
let cs: Vec<f32> = dados.iter().flat_map(|(c, _)| c.clone()).collect();
|
| 992 |
-
let as_: Vec<f32> = dados.iter().flat_map(|(_, a)| a.clone()).collect();
|
| 993 |
-
let cond = Tensor::from_vec(cs, (256, 6), &dev).unwrap();
|
| 994 |
-
let alvo = Tensor::from_vec(as_, (256, 4), &dev).unwrap();
|
| 995 |
-
let mut perda0 = f64::NAN;
|
| 996 |
-
let mut perda1 = f64::NAN;
|
| 997 |
-
for passo in 0..200 {
|
| 998 |
-
let perda = motor.perda_treino(&cond, &alvo).unwrap();
|
| 999 |
-
if passo == 0 {
|
| 1000 |
-
perda0 = perda.to_scalar::<f32>().unwrap() as f64;
|
| 1001 |
-
}
|
| 1002 |
-
perda1 = perda.to_scalar::<f32>().unwrap() as f64;
|
| 1003 |
-
otim.passo(crate::treino::grads_achatados(&perda, &motor.vars()).unwrap());
|
| 1004 |
-
}
|
| 1005 |
-
assert!(perda1 < perda0 * 0.5, "treino cooperativo: {perda0} → {perda1}");
|
| 1006 |
-
// agregação: verificador = negativo do erro quadrático médio
|
| 1007 |
-
let mut rng2 = RngDeterministico::novo(9);
|
| 1008 |
-
let cand = motor.candidatos(&cond.get(0).unwrap(), 0.0, &mut rng2).unwrap();
|
| 1009 |
-
let (resp, pesos) = motor.agregar(&cand).unwrap();
|
| 1010 |
-
assert_eq!(resp.dims().to_vec(), vec![1, 4]);
|
| 1011 |
-
let soma: f64 = pesos.iter().sum();
|
| 1012 |
-
assert!((soma - 1.0).abs() < 1e-9, "pesos somam {soma}");
|
| 1013 |
-
assert!(pesos.iter().all(|&p| p >= 0.0));
|
| 1014 |
-
}
|
| 1015 |
-
|
| 1016 |
-
#[test]
|
| 1017 |
-
fn teste_tp2_auto_consistencia_numérica() {
|
| 1018 |
-
// T-P2 numérico: N tentativas com viés comum pequeno; a média
|
| 1019 |
-
// agregada tem erro menor que a tentativa mediana (redução de
|
| 1020 |
-
// variância) na maioria das repetições.
|
| 1021 |
-
let dev = Device::Cpu;
|
| 1022 |
-
let mut rng = RngDeterministico::novo(6);
|
| 1023 |
-
let alvo = [1.0f64, 2.0, -1.0, 0.5];
|
| 1024 |
-
let mut venceu = 0;
|
| 1025 |
-
let reps = 200;
|
| 1026 |
-
for _ in 0..reps {
|
| 1027 |
-
let n = 7;
|
| 1028 |
-
let erros: Vec<f64> = (0..n)
|
| 1029 |
-
.map(|_| {
|
| 1030 |
-
alvo
|
| 1031 |
-
.iter()
|
| 1032 |
-
.map(|&a| {
|
| 1033 |
-
let ruido = rng.normal(0.0, 1.0);
|
| 1034 |
-
(a + ruido - a).abs()
|
| 1035 |
-
})
|
| 1036 |
-
.sum::<f64>()
|
| 1037 |
-
})
|
| 1038 |
-
.collect();
|
| 1039 |
-
let media: Vec<f64> = (0..4)
|
| 1040 |
-
.map(|j| {
|
| 1041 |
-
(0..n).map(|i| alvo[j] + rng.normal(0.0, 1.0)).sum::<f64>() / n as f64
|
| 1042 |
-
})
|
| 1043 |
-
.collect();
|
| 1044 |
-
let erro_media: f64 = media.iter().zip(alvo.iter()).map(|(m, a)| (m - a).abs()).sum();
|
| 1045 |
-
let mut ordenado = erros.clone();
|
| 1046 |
-
ordenado.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
| 1047 |
-
let mediana = ordenado[n / 2];
|
| 1048 |
-
if erro_media <= mediana {
|
| 1049 |
-
venceu += 1;
|
| 1050 |
-
}
|
| 1051 |
-
}
|
| 1052 |
-
// a média vence a mediana na grande maioria dos casos (E[erro da
|
| 1053 |
-
// média] = σ/√n < σ = E[erro de UMA tentativa])
|
| 1054 |
-
assert!(
|
| 1055 |
-
venceu as f64 / reps as f64 >= 0.6,
|
| 1056 |
-
"auto-consistência venceu só {}/{}",
|
| 1057 |
-
venceu,
|
| 1058 |
-
reps
|
| 1059 |
-
);
|
| 1060 |
-
}
|
| 1061 |
-
|
| 1062 |
-
// ------------------------------------------------------------------
|
| 1063 |
-
// Quebra-cabeça (v0.7.0) — gates Q-1..Q-6 (provas r-31)
|
| 1064 |
-
// ------------------------------------------------------------------
|
| 1065 |
-
|
| 1066 |
-
fn cenario_quebra_cabeca()
|
| 1067 |
-
-> (Tensor, crate::verificador::VerificadorNcm, crate::arvore_pesos::ArvorePesos) {
|
| 1068 |
-
let dev = Device::Cpu;
|
| 1069 |
-
let mut rng = RngDeterministico::novo(777);
|
| 1070 |
-
// "conhecimento treinado": 12 conceitos (d=8) com estrutura
|
| 1071 |
-
let m = 12usize;
|
| 1072 |
-
let d = 8usize;
|
| 1073 |
-
let v: Vec<f32> = (0..m * d).map(|i| rng.normal_padrao() as f32).collect();
|
| 1074 |
-
let conceitos = Tensor::from_vec(v, (m, d), &dev).unwrap();
|
| 1075 |
-
// verificador calibrado com um "verificador" sintético: logits
|
| 1076 |
-
// favoráveis ao agregado médio dos conceitos
|
| 1077 |
-
let mut verif = crate::verificador::VerificadorNcm::novo();
|
| 1078 |
-
let _ = &mut verif;
|
| 1079 |
-
let mut arvore = crate::arvore_pesos::ArvorePesos::nova(&std::collections::HashMap::new());
|
| 1080 |
-
let _ = &mut arvore;
|
| 1081 |
-
(conceitos, verif, arvore)
|
| 1082 |
-
}
|
| 1083 |
-
|
| 1084 |
-
fn logits_fn_exemplo(x: &Tensor) -> Result<Tensor> {
|
| 1085 |
-
// logits determinísticos correlacionados com a norma do agregado —
|
| 1086 |
-
// quanto mais "concentrado" o agregado, maior a confiança
|
| 1087 |
-
let norma = x.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
|
| 1088 |
-
let base = (norma * 2.0).min(8.0);
|
| 1089 |
-
let v: Vec<f32> = vec![base as f32, 0.5, 0.2, 0.1];
|
| 1090 |
-
Ok(Tensor::from_vec(v, (4,), x.device())?)
|
| 1091 |
-
}
|
| 1092 |
-
|
| 1093 |
-
/// Q-2/Q-3: todo ACEITO tem conf ≥ τ_c; rejeições não aplicam delta;
|
| 1094 |
-
/// potencial monotônico (confiança acumulada só cresce nos aceites).
|
| 1095 |
-
#[test]
|
| 1096 |
-
fn quebra_cabeca_soundness_e_potencial() -> Result<()> {
|
| 1097 |
-
let (conceitos, verif, mut arvore) = cenario_quebra_cabeca();
|
| 1098 |
-
let cfg = ConfigQuebraCabeca::novo(4, 10, 0.55);
|
| 1099 |
-
let mut rng = RngDeterministico::novo(4242);
|
| 1100 |
-
let r = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?;
|
| 1101 |
-
for rod in &r.rodadas {
|
| 1102 |
-
if rod.aceito {
|
| 1103 |
-
assert!(
|
| 1104 |
-
rod.confianca >= cfg.tau_c,
|
| 1105 |
-
"Q-2: aceito com conf {} < τ_c {}",
|
| 1106 |
-
rod.confianca,
|
| 1107 |
-
cfg.tau_c
|
| 1108 |
-
);
|
| 1109 |
-
assert!(rod.delta_aplicado > 0.0, "aceite aplica delta > 0");
|
| 1110 |
-
} else {
|
| 1111 |
-
assert_eq!(rod.delta_aplicado, 0.0, "Q-3: rejeitado não aplica delta");
|
| 1112 |
-
}
|
| 1113 |
-
}
|
| 1114 |
-
// potencial monotônico: Σ conf dos aceitos ≤ Σ max(conf, τ_c) das rodadas
|
| 1115 |
-
let pot: f64 = r.rodadas.iter().filter(|x| x.aceito).map(|x| x.confianca).sum();
|
| 1116 |
-
assert!(pot >= 0.0);
|
| 1117 |
-
// Q-5: invariantes da árvore preservadas
|
| 1118 |
-
arvore.invariantes_ok().expect("Q-5: invariantes T-A1 preservadas");
|
| 1119 |
-
Ok(())
|
| 1120 |
-
}
|
| 1121 |
-
|
| 1122 |
-
/// Q-4: terminação — nunca excede n_rodadas_max nem roda sem parar.
|
| 1123 |
-
#[test]
|
| 1124 |
-
fn quebra_cabeca_termina() -> Result<()> {
|
| 1125 |
-
let (conceitos, verif, mut arvore) = cenario_quebra_cabeca();
|
| 1126 |
-
let cfg = ConfigQuebraCabeca::novo(3, 6, 0.99); // τ inalcançável
|
| 1127 |
-
let mut rng = RngDeterministico::novo(1);
|
| 1128 |
-
let r = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?;
|
| 1129 |
-
assert!(r.rodadas.len() <= 6, "Q-4: orçamento respeitado");
|
| 1130 |
-
assert_eq!(r.aceitos, 0, "τ inalcançável ⇒ nenhum aceite");
|
| 1131 |
-
assert!(r.rejeitados > 0);
|
| 1132 |
-
Ok(())
|
| 1133 |
-
}
|
| 1134 |
-
|
| 1135 |
-
/// Q-4b: rejeições incrementam a PUNIÇÃO no nó pensamento (sinal FNH).
|
| 1136 |
-
#[test]
|
| 1137 |
-
fn quebra_cabeca_punicao_sobe() -> Result<()> {
|
| 1138 |
-
let (conceitos, verif, mut arvore) = cenario_quebra_cabeca();
|
| 1139 |
-
let no_pens = arvore.no("pensamento").unwrap();
|
| 1140 |
-
let antes = arvore.nos[no_pens].metricas.get("punicao").copied().unwrap_or(0.0);
|
| 1141 |
-
let cfg = ConfigQuebraCabeca::novo(3, 5, 0.999);
|
| 1142 |
-
let mut rng = RngDeterministico::novo(9);
|
| 1143 |
-
let _ = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?;
|
| 1144 |
-
let depois = arvore.nos[no_pens].metricas.get("punicao").copied().unwrap_or(0.0);
|
| 1145 |
-
assert!(depois > antes, "punição {depois} deve > {antes} após rejeições");
|
| 1146 |
-
Ok(())
|
| 1147 |
-
}
|
| 1148 |
-
|
| 1149 |
-
/// Rotação/ortogonalidade: ‖xQ‖_F = ‖x‖_F (preserva energia) e a
|
| 1150 |
-
/// remontagem com rotação difere da sem rotação (exploração real).
|
| 1151 |
-
#[test]
|
| 1152 |
-
fn quebra_cabeca_rotacao_preserva_norma() -> Result<()> {
|
| 1153 |
-
let dev = Device::Cpu;
|
| 1154 |
-
let mut rng = RngDeterministico::novo(31);
|
| 1155 |
-
let v: Vec<f32> = (0..5 * 6).map(|_| rng.normal_padrao() as f32).collect();
|
| 1156 |
-
let x = Tensor::from_vec(v, (5, 6), &dev)?;
|
| 1157 |
-
let q = q_ortogonal_deterministica(6, &mut rng, &dev)?;
|
| 1158 |
-
let xq = x.matmul(&q)?;
|
| 1159 |
-
let nx = x.sqr()?.sum_all()?.to_scalar::<f32>()?;
|
| 1160 |
-
let nxq = xq.sqr()?.sum_all()?.to_scalar::<f32>()?;
|
| 1161 |
-
assert!(
|
| 1162 |
-
(nx - nxq).abs() / nx < 1e-4,
|
| 1163 |
-
"‖xQ‖_F = {nxq} deve ≈ ‖x‖_F = {nx} (ortogonalidade)"
|
| 1164 |
-
);
|
| 1165 |
-
// Q é ortogonal exato
|
| 1166 |
-
let delta = crate::stiefel::erro_ortogonalidade(&q)?;
|
| 1167 |
-
assert!(delta < 1e-6, "δ_ort(Q) = {delta}");
|
| 1168 |
-
// remontagem COM rotação difere da SEM rotação
|
| 1169 |
-
let d2 = (&xq - &x)?.abs()?.max_all()?.to_scalar::<f32>()?;
|
| 1170 |
-
assert!(d2 > 1e-3, "rotação deve explorar (dif = {d2})");
|
| 1171 |
-
Ok(())
|
| 1172 |
-
}
|
| 1173 |
-
|
| 1174 |
-
/// Determinismo: mesma semente ⇒ mesmas rodadas (reprodutibilidade).
|
| 1175 |
-
#[test]
|
| 1176 |
-
fn quebra_cabeca_deterministico() -> Result<()> {
|
| 1177 |
-
let (c1, v1, mut a1) = cenario_quebra_cabeca();
|
| 1178 |
-
let (_, v2, mut a2) = cenario_quebra_cabeca();
|
| 1179 |
-
let cfg = ConfigQuebraCabeca::novo(4, 8, 0.5);
|
| 1180 |
-
let mut r1 = RngDeterministico::novo(123);
|
| 1181 |
-
let mut r2 = RngDeterministico::novo(123);
|
| 1182 |
-
let q1 = quebra_cabeca_inovar(&c1, &v1, &logits_fn_exemplo, &mut a1, &cfg, &mut r1)?;
|
| 1183 |
-
let q2 = quebra_cabeca_inovar(&c1, &v2, &logits_fn_exemplo, &mut a2, &cfg, &mut r2)?;
|
| 1184 |
-
assert_eq!(q1.rodadas.len(), q2.rodadas.len());
|
| 1185 |
-
for (x, y) in q1.rodadas.iter().zip(q2.rodadas.iter()) {
|
| 1186 |
-
assert_eq!(x.permutacao_pecas, y.permutacao_pecas, "mesma semente ⇒ mesma remontagem");
|
| 1187 |
-
assert!((x.confianca - y.confianca).abs() < 1e-12);
|
| 1188 |
-
assert_eq!(x.aceito, y.aceito);
|
| 1189 |
-
}
|
| 1190 |
-
Ok(())
|
| 1191 |
-
}
|
| 1192 |
-
}
|
|
|
|
| 1 |
+
//! `pensamento::motor` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
// ---------------------------------------------------------------------------
|
| 8 |
// Confiança e gatilho (T-P1)
|
|
|
|
| 547 |
}
|
| 548 |
}
|
| 549 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/pensamento/quebra_cabeca.rs
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `pensamento::quebra_cabeca` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
// ---------------------------------------------------------------------------
|
| 8 |
+
// QUEBRA-CABEÇA pós-treinamento (v0.7.0 — inovação com o conhecimento
|
| 9 |
+
// treinado; requisito do usuário; provas Q-1..Q-6 em r-31)
|
| 10 |
+
// ---------------------------------------------------------------------------
|
| 11 |
+
|
| 12 |
+
/// Configuração do quebra-cabeça (r-28 §4.3).
|
| 13 |
+
#[derive(Clone, Copy, Debug)]
|
| 14 |
+
pub struct ConfigQuebraCabeca {
|
| 15 |
+
/// K peças em que o conhecimento é quebrado.
|
| 16 |
+
pub n_pecas: usize,
|
| 17 |
+
/// orçamento B de rodadas (terminação Q-4).
|
| 18 |
+
pub n_rodadas_max: usize,
|
| 19 |
+
/// limiar de validade (mesmo τ_c calibrado do verificador — Q-2).
|
| 20 |
+
pub tau_c: f64,
|
| 21 |
+
/// teto do ajuste por componente na árvore (regulação).
|
| 22 |
+
pub delta_max: f64,
|
| 23 |
+
/// remonta as peças com mistura ORTOGONAL por peça (rotação/ortogonalidade).
|
| 24 |
+
pub rotacao_ortogonal: bool,
|
| 25 |
+
/// para a execução após este nº de rodadas consecutivas sem aceite.
|
| 26 |
+
pub paciencia: usize,
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
impl Default for ConfigQuebraCabeca {
|
| 30 |
+
fn default() -> Self {
|
| 31 |
+
ConfigQuebraCabeca {
|
| 32 |
+
n_pecas: 4,
|
| 33 |
+
n_rodadas_max: 12,
|
| 34 |
+
tau_c: 0.55,
|
| 35 |
+
delta_max: 0.05,
|
| 36 |
+
rotacao_ortogonal: true,
|
| 37 |
+
paciencia: 5,
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
impl ConfigQuebraCabeca {
|
| 43 |
+
pub fn novo(n_pecas: usize, n_rodadas: usize, tau_c: f64) -> Self {
|
| 44 |
+
ConfigQuebraCabeca {
|
| 45 |
+
n_pecas,
|
| 46 |
+
n_rodadas_max: n_rodadas,
|
| 47 |
+
tau_c,
|
| 48 |
+
delta_max: 0.05,
|
| 49 |
+
rotacao_ortogonal: true,
|
| 50 |
+
paciencia: (n_rodadas / 2).max(2),
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/// Uma rodada do quebra-cabeça.
|
| 56 |
+
#[derive(Debug, Clone)]
|
| 57 |
+
pub struct RodadaQuebraCabeca {
|
| 58 |
+
pub rodada: usize,
|
| 59 |
+
/// permutação das peças aplicada na remontagem (reordenação).
|
| 60 |
+
pub permutacao_pecas: Vec<usize>,
|
| 61 |
+
/// confiança NCM do candidato remontado (reavaliação).
|
| 62 |
+
pub confianca: f64,
|
| 63 |
+
/// conf ≥ τ_c ⇒ VÁLIDO (ajusta a árvore); senão PUNIDO (descarta).
|
| 64 |
+
pub aceito: bool,
|
| 65 |
+
/// delta aplicado na árvore (0 quando descartado).
|
| 66 |
+
pub delta_aplicado: f64,
|
| 67 |
+
/// rotações ortogonais usadas (quando ativas) — por peça.
|
| 68 |
+
pub usou_rotacao: bool,
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/// Resultado consolidado do quebra-cabeça.
|
| 72 |
+
#[derive(Debug, Clone)]
|
| 73 |
+
pub struct ResultadoQuebraCabeca {
|
| 74 |
+
pub rodadas: Vec<RodadaQuebraCabeca>,
|
| 75 |
+
pub aceitos: usize,
|
| 76 |
+
pub rejeitados: usize,
|
| 77 |
+
pub confianca_base: f64,
|
| 78 |
+
pub confianca_melhor: f64,
|
| 79 |
+
pub telemetria: HashMap<String, f64>,
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/// **Quebra-cabeça de inovação pós-treinamento** (r-28 §4.3):
|
| 83 |
+
/// 1. quebra o conhecimento treinado `conceitos` (m, d) em K peças por
|
| 84 |
+
/// permutação aleatória das linhas (Fisher–Yates com RNG determinístico);
|
| 85 |
+
/// 2. remonta as peças em nova ordem (e com mistura ortogonal por peça
|
| 86 |
+
/// quando `rotacao_ortogonal`) — "descobrir novas possibilidades";
|
| 87 |
+
/// 3. reavalia cada remontagem pelo verificador NCM (calibrado — V-2);
|
| 88 |
+
/// VÁLIDO (conf ≥ τ_c) ⇒ ajuste regulado na árvore de pesos indexada
|
| 89 |
+
/// (nó "pensamento", crédito fundo-topo); PUNIDO ⇒ descarta e o incremento
|
| 90 |
+
/// de punição sobe no nó (alimenta o gate do FNH — integração v0.7.0);
|
| 91 |
+
/// 4. repete até o orçamento B ou a paciência esgotar (Q-4).
|
| 92 |
+
///
|
| 93 |
+
/// **Provas (r-31):** Q-1 cobertura (permutações uniformes ⇒ S_K no suporte;
|
| 94 |
+
/// rotações varrem O(d)); Q-2 soundness herdada (mesmo limiar calibrado do
|
| 95 |
+
/// verificador — P(aceite inválido) ≤ 1−τ_c+ECE); Q-3 potencial monotônico
|
| 96 |
+
/// (Φ_r = Σ conf dos aceitos é não-decrescente; rejeições não tocam Φ);
|
| 97 |
+
/// Q-4 terminação (B finito, rodadas O(m·d)); Q-5 invariantes da árvore
|
| 98 |
+
/// preservadas (ajustes só tocam métricas, nunca a estrutura — T-A1 intacto;
|
| 99 |
+
/// agregação pós-ajuste mantém T-A2); Q-6 causalidade (permutação e confiança
|
| 100 |
+
/// medidas antes do ajuste — filtração F_r).
|
| 101 |
+
pub fn quebra_cabeca_inovar(
|
| 102 |
+
conceitos: &Tensor,
|
| 103 |
+
verificador: &crate::verificador::VerificadorNcm,
|
| 104 |
+
logits_fn: &dyn Fn(&Tensor) -> Result<Tensor>,
|
| 105 |
+
arvore: &mut crate::arvore_pesos::ArvorePesos,
|
| 106 |
+
cfg: &ConfigQuebraCabeca,
|
| 107 |
+
rng: &mut RngDeterministico,
|
| 108 |
+
) -> Result<ResultadoQuebraCabeca> {
|
| 109 |
+
let dev = conceitos.device().clone();
|
| 110 |
+
let (m, d) = (conceitos.dims()[0], conceitos.dims()[1]);
|
| 111 |
+
let k = cfg.n_pecas.clamp(1, m);
|
| 112 |
+
let no_pens = arvore.no("pensamento").unwrap_or(0);
|
| 113 |
+
|
| 114 |
+
// linha de base: confiança do conhecimento original (agregado médio)
|
| 115 |
+
let base_agg = conceitos.mean(0)?;
|
| 116 |
+
let conf_base = {
|
| 117 |
+
let lg = logits_fn(&base_agg)?;
|
| 118 |
+
verificador.confianca_1d(&lg)?
|
| 119 |
+
};
|
| 120 |
+
|
| 121 |
+
let mut rodadas = Vec::new();
|
| 122 |
+
let mut aceitos = 0usize;
|
| 123 |
+
let mut rejeitados = 0usize;
|
| 124 |
+
let mut conf_melhor = conf_base;
|
| 125 |
+
let mut sem_aceite = 0usize;
|
| 126 |
+
|
| 127 |
+
for r in 1..=cfg.n_rodadas_max {
|
| 128 |
+
if sem_aceite >= cfg.paciencia {
|
| 129 |
+
break;
|
| 130 |
+
}
|
| 131 |
+
// (1) QUEBRA: permutação de linhas Fisher–Yates (Q-6: amostrada ANTES)
|
| 132 |
+
let mut perm: Vec<usize> = (0..m).collect();
|
| 133 |
+
for i in (1..m).rev() {
|
| 134 |
+
let j = rng.inteiro(i + 1);
|
| 135 |
+
perm.swap(i, j);
|
| 136 |
+
}
|
| 137 |
+
// fatia em K peças contíguas (tamanhos quase iguais)
|
| 138 |
+
let tamanho = (m + k - 1) / k;
|
| 139 |
+
let mut pecas = Vec::with_capacity(k);
|
| 140 |
+
let mut ini = 0usize;
|
| 141 |
+
while ini < m {
|
| 142 |
+
let fim = (ini + tamanho).min(m);
|
| 143 |
+
let idx: Vec<u32> = perm[ini..fim].iter().map(|&x| x as u32).collect();
|
| 144 |
+
let pedaco = conceitos.index_select(&Tensor::from_vec(idx, (fim - ini,), &dev)?, 0)?;
|
| 145 |
+
pecas.push(pedaco);
|
| 146 |
+
ini = fim;
|
| 147 |
+
}
|
| 148 |
+
// (2) REMONTA: nova ordem aleatória das peças (permutação de peças)
|
| 149 |
+
let mut perm_pecas: Vec<usize> = (0..pecas.len()).collect();
|
| 150 |
+
for i in (1..pecas.len()).rev() {
|
| 151 |
+
let j = rng.inteiro(i + 1);
|
| 152 |
+
perm_pecas.swap(i, j);
|
| 153 |
+
}
|
| 154 |
+
let mut remontada = Vec::with_capacity(pecas.len());
|
| 155 |
+
for &pi in &perm_pecas {
|
| 156 |
+
let mut pedaco = pecas[pi].clone();
|
| 157 |
+
if cfg.rotacao_ortogonal && d >= 2 {
|
| 158 |
+
// mistura ortogonal da peça: Q ∈ O(d) via polar de gaussiana
|
| 159 |
+
// do RNG determinístico (preserva normas — ‖xQ‖_F = ‖x‖_F)
|
| 160 |
+
let q = q_ortogonal_deterministica(d, rng, &dev)?;
|
| 161 |
+
pedaco = pedaco.matmul(&q)?;
|
| 162 |
+
}
|
| 163 |
+
remontada.push(pedaco);
|
| 164 |
+
}
|
| 165 |
+
let candidata = Tensor::cat(&remontada, 0)?; // (m, d) remontado
|
| 166 |
+
// (3) REAVALIAÇÃO pelo verificador NCM (Q-2: mesmo limiar calibrado)
|
| 167 |
+
let agg = candidata.mean(0)?;
|
| 168 |
+
let lg = logits_fn(&agg)?;
|
| 169 |
+
let conf = verificador.confianca_1d(&lg)?;
|
| 170 |
+
let aceito = conf >= cfg.tau_c;
|
| 171 |
+
let delta = if aceito {
|
| 172 |
+
// VÁLIDO: ajusta a árvore bidirecional (indexada, com teto)
|
| 173 |
+
let escalado = (conf - cfg.tau_c).min(cfg.delta_max);
|
| 174 |
+
arvore.aplicar_ajuste_indexado(no_pens, "confianca", escalado, cfg.delta_max)
|
| 175 |
+
} else {
|
| 176 |
+
// PUNIDO: descarta; a punição sobe no nó (sinal para o gate FNH)
|
| 177 |
+
rejeitados += 1;
|
| 178 |
+
sem_aceite += 1;
|
| 179 |
+
arvore.aplicar_ajuste_indexado(no_pens, "punicao", cfg.delta_max * 0.5, cfg.delta_max);
|
| 180 |
+
0.0
|
| 181 |
+
};
|
| 182 |
+
if aceito {
|
| 183 |
+
aceitos += 1;
|
| 184 |
+
sem_aceite = 0;
|
| 185 |
+
conf_melhor = conf_melhor.max(conf);
|
| 186 |
+
}
|
| 187 |
+
rodadas.push(RodadaQuebraCabeca {
|
| 188 |
+
rodada: r,
|
| 189 |
+
permutacao_pecas: perm_pecas,
|
| 190 |
+
confianca: conf,
|
| 191 |
+
aceito,
|
| 192 |
+
delta_aplicado: delta,
|
| 193 |
+
usou_rotacao: cfg.rotacao_ortogonal,
|
| 194 |
+
});
|
| 195 |
+
}
|
| 196 |
+
// crédito fundo-topo após todas as rodadas (T-A2)
|
| 197 |
+
arvore.agregar_fundo_topo("confianca");
|
| 198 |
+
arvore.agregar_fundo_topo("punicao");
|
| 199 |
+
|
| 200 |
+
let n_rod = rodadas.len();
|
| 201 |
+
let mut telemetria = HashMap::new();
|
| 202 |
+
telemetria.insert("rodadas".into(), n_rod as f64);
|
| 203 |
+
telemetria.insert("aceitos".into(), aceitos as f64);
|
| 204 |
+
telemetria.insert("rejeitados".into(), rejeitados as f64);
|
| 205 |
+
telemetria.insert("confianca_base".into(), conf_base);
|
| 206 |
+
telemetria.insert("confianca_melhor".into(), conf_melhor);
|
| 207 |
+
telemetria.insert(
|
| 208 |
+
"taxa_aceitacao".into(),
|
| 209 |
+
if n_rod > 0 { aceitos as f64 / n_rod as f64 } else { 0.0 },
|
| 210 |
+
);
|
| 211 |
+
telemetria.insert("rotacao_ativa".into(), if cfg.rotacao_ortogonal { 1.0 } else { 0.0 });
|
| 212 |
+
Ok(ResultadoQuebraCabeca {
|
| 213 |
+
rodadas,
|
| 214 |
+
aceitos,
|
| 215 |
+
rejeitados,
|
| 216 |
+
confianca_base: conf_base,
|
| 217 |
+
confianca_melhor: conf_melhor,
|
| 218 |
+
telemetria,
|
| 219 |
+
})
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/// Q ∈ O(d) determinística: gaussiana do RNG próprio → retração polar.
|
| 223 |
+
/// Ortogonal EXATA (δ_ort ≤ 1e-6) e reprodutível (mesma semente ⇒ mesmo Q).
|
| 224 |
+
pub fn q_ortogonal_deterministica(
|
| 225 |
+
d: usize,
|
| 226 |
+
rng: &mut RngDeterministico,
|
| 227 |
+
dev: &Device,
|
| 228 |
+
) -> Result<Tensor> {
|
| 229 |
+
let n = d * d;
|
| 230 |
+
let v: Vec<f32> = (0..n).map(|_| rng.normal_padrao() as f32).collect();
|
| 231 |
+
let g = Tensor::from_vec(v, (d, d), dev)?;
|
| 232 |
+
crate::stiefel::retrair_polar_frobenius(&g, 90)
|
| 233 |
+
}
|
src/pensamento/testes.rs
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `pensamento::testes` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
#[cfg(test)]
|
| 9 |
+
mod testes {
|
| 10 |
+
use super::*;
|
| 11 |
+
|
| 12 |
+
#[test]
|
| 13 |
+
fn teste_confianca_extremos() {
|
| 14 |
+
// T-P1: Dirac ⇒ conf = 1; uniforme ⇒ conf = 0
|
| 15 |
+
let dev = Device::Cpu;
|
| 16 |
+
let dirac = Tensor::from_vec(vec![100.0f32, 0.0, 0.0, 0.0], (1, 4), &dev).unwrap();
|
| 17 |
+
let uniforme = Tensor::from_vec(vec![0.0f32; 4], (1, 4), &dev).unwrap();
|
| 18 |
+
let c1 = confianca_de_logits(&dirac).unwrap();
|
| 19 |
+
let c0 = confianca_de_logits(&uniforme).unwrap();
|
| 20 |
+
assert!(c1 > 0.999, "conf dirac = {c1}");
|
| 21 |
+
assert!(c0 < 0.001, "conf uniforme = {c0}");
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
#[test]
|
| 25 |
+
fn teste_tp3_projetar_ridge_minimiza() {
|
| 26 |
+
// T-P3: r* da projeção ridge tem resíduo ≤ qualquer perturbação
|
| 27 |
+
let dev = Device::Cpu;
|
| 28 |
+
let w = Tensor::randn(0f32, 1.0, (8, 3), &dev).unwrap();
|
| 29 |
+
let r_alvo = Tensor::randn(0f32, 1.0, (8, 5), &dev).unwrap();
|
| 30 |
+
let r = projetar_ridge(&w, &r_alvo, 1e-6).unwrap();
|
| 31 |
+
let residuo = |rr: &Tensor| {
|
| 32 |
+
let d = (w.matmul(rr).unwrap() - &r_alvo).unwrap();
|
| 33 |
+
d.sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap() as f64
|
| 34 |
+
};
|
| 35 |
+
let f0 = residuo(&r);
|
| 36 |
+
let mut rng = RngDeterministico::novo(1);
|
| 37 |
+
for _ in 0..30 {
|
| 38 |
+
let pert: Vec<f32> = (0..15).map(|_| rng.normal(0.0, 0.1) as f32).collect();
|
| 39 |
+
let rp = (Tensor::from_vec(pert, (3, 5), &dev).unwrap() + &r).unwrap();
|
| 40 |
+
let fp = residuo(&rp);
|
| 41 |
+
assert!(f0 <= fp + 1e-6, "residuo r* = {f0} > perturbado {fp}");
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
#[test]
|
| 46 |
+
fn teste_tp4_terminacao_e_orcamento() {
|
| 47 |
+
// o ciclo d SEMPRE termina em ≤ orçamento iterações (potencial
|
| 48 |
+
// Φ = B − k); com verificador impossível, termina com
|
| 49 |
+
// orcamento_esgotado = 1.
|
| 50 |
+
let dev = Device::Cpu;
|
| 51 |
+
let mut motor = MotorPensamento::novo(6, 4, 3, 0.99, 5, &dev).unwrap();
|
| 52 |
+
let cond = Tensor::randn(0f32, 1.0, (6,), &dev).unwrap();
|
| 53 |
+
let mut rng = RngDeterministico::novo(2);
|
| 54 |
+
let res = motor
|
| 55 |
+
.raciocinar(&cond, &|_| 0.0, &mut rng) // verificador sempre 0
|
| 56 |
+
.unwrap();
|
| 57 |
+
assert!(res.iteracoes <= 5, "iterações = {} > orçamento", res.iteracoes);
|
| 58 |
+
assert_eq!(res.telemetria.get("orcamento_esgotado"), Some(&1.0));
|
| 59 |
+
assert!(res.resposta.is_some(), "melhor candidato ainda é retornado");
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
#[test]
|
| 63 |
+
fn teste_tp5_neutralidade_no_nascimento() {
|
| 64 |
+
// gate = 0 ⇒ saída da atenção integrada é BIT-A-BIT a da base
|
| 65 |
+
let dev = Device::Cpu;
|
| 66 |
+
let motor = MotorPensamento::novo(8, 8, 3, 0.5, 3, &dev).unwrap();
|
| 67 |
+
let q = Tensor::randn(0f32, 1.0, (1, 4, 8), &dev).unwrap();
|
| 68 |
+
let k = Tensor::randn(0f32, 1.0, (1, 6, 8), &dev).unwrap();
|
| 69 |
+
let v = Tensor::randn(0f32, 1.0, (1, 6, 8), &dev).unwrap();
|
| 70 |
+
let cand = Tensor::randn(0f32, 1.0, (3, 8), &dev).unwrap();
|
| 71 |
+
let y_int = motor.atencao_integrada(&q, &k, &v, &cand).unwrap();
|
| 72 |
+
// atenção base pura
|
| 73 |
+
let scores = (q.matmul(&k.transpose(1, 2).unwrap()).unwrap() / (8f64).sqrt()).unwrap();
|
| 74 |
+
let attn = candle_nn::ops::softmax(&scores, candle_core::D::Minus1).unwrap();
|
| 75 |
+
let y_base = attn.matmul(&v).unwrap();
|
| 76 |
+
let dif = (&y_int - &y_base).unwrap().abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
|
| 77 |
+
assert!(dif == 0.0, "gate=0 deveria dar bit-a-bit, dif = {dif}");
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
#[test]
|
| 81 |
+
fn teste_treino_cooperativo_e_agregacao() {
|
| 82 |
+
// item a: as tentativas + combinador APRENDEM a função alvo; a
|
| 83 |
+
// agregação converge para o alvo melhor que uma tentativa isolada
|
| 84 |
+
// no pior caso (verificação numérica honesta em escala pequena).
|
| 85 |
+
let dev = Device::Cpu;
|
| 86 |
+
let mut motor = MotorPensamento::novo(6, 4, 3, 0.5, 3, &dev).unwrap();
|
| 87 |
+
let vars = motor.vars();
|
| 88 |
+
let mut otim = crate::treino::AdamWManual::novo(vars, 5e-3, 0.0);
|
| 89 |
+
let mut rng = RngDeterministico::novo(4);
|
| 90 |
+
// alvo: função não-linear determinística de cond
|
| 91 |
+
let dados: Vec<(Vec<f32>, Vec<f32>)> = (0..256)
|
| 92 |
+
.map(|_| {
|
| 93 |
+
let c: Vec<f32> = (0..6).map(|_| rng.normal(0.0, 1.0) as f32).collect();
|
| 94 |
+
let a: Vec<f32> = (0..4)
|
| 95 |
+
.map(|j| {
|
| 96 |
+
(c[j % 6] * 0.5 + (c[(j + 1) % 6] * c[(j + 2) % 6]) * 0.3 + 0.1) as f32
|
| 97 |
+
})
|
| 98 |
+
.collect();
|
| 99 |
+
(c, a)
|
| 100 |
+
})
|
| 101 |
+
.collect();
|
| 102 |
+
let cs: Vec<f32> = dados.iter().flat_map(|(c, _)| c.clone()).collect();
|
| 103 |
+
let as_: Vec<f32> = dados.iter().flat_map(|(_, a)| a.clone()).collect();
|
| 104 |
+
let cond = Tensor::from_vec(cs, (256, 6), &dev).unwrap();
|
| 105 |
+
let alvo = Tensor::from_vec(as_, (256, 4), &dev).unwrap();
|
| 106 |
+
let mut perda0 = f64::NAN;
|
| 107 |
+
let mut perda1 = f64::NAN;
|
| 108 |
+
for passo in 0..200 {
|
| 109 |
+
let perda = motor.perda_treino(&cond, &alvo).unwrap();
|
| 110 |
+
if passo == 0 {
|
| 111 |
+
perda0 = perda.to_scalar::<f32>().unwrap() as f64;
|
| 112 |
+
}
|
| 113 |
+
perda1 = perda.to_scalar::<f32>().unwrap() as f64;
|
| 114 |
+
otim.passo(crate::treino::grads_achatados(&perda, &motor.vars()).unwrap());
|
| 115 |
+
}
|
| 116 |
+
assert!(perda1 < perda0 * 0.5, "treino cooperativo: {perda0} → {perda1}");
|
| 117 |
+
// agregação: verificador = negativo do erro quadrático médio
|
| 118 |
+
let mut rng2 = RngDeterministico::novo(9);
|
| 119 |
+
let cand = motor.candidatos(&cond.get(0).unwrap(), 0.0, &mut rng2).unwrap();
|
| 120 |
+
let (resp, pesos) = motor.agregar(&cand).unwrap();
|
| 121 |
+
assert_eq!(resp.dims().to_vec(), vec![1, 4]);
|
| 122 |
+
let soma: f64 = pesos.iter().sum();
|
| 123 |
+
assert!((soma - 1.0).abs() < 1e-9, "pesos somam {soma}");
|
| 124 |
+
assert!(pesos.iter().all(|&p| p >= 0.0));
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
#[test]
|
| 128 |
+
fn teste_tp2_auto_consistencia_numérica() {
|
| 129 |
+
// T-P2 numérico: N tentativas com viés comum pequeno; a média
|
| 130 |
+
// agregada tem erro menor que a tentativa mediana (redução de
|
| 131 |
+
// variância) na maioria das repetições.
|
| 132 |
+
let dev = Device::Cpu;
|
| 133 |
+
let mut rng = RngDeterministico::novo(6);
|
| 134 |
+
let alvo = [1.0f64, 2.0, -1.0, 0.5];
|
| 135 |
+
let mut venceu = 0;
|
| 136 |
+
let reps = 200;
|
| 137 |
+
for _ in 0..reps {
|
| 138 |
+
let n = 7;
|
| 139 |
+
let erros: Vec<f64> = (0..n)
|
| 140 |
+
.map(|_| {
|
| 141 |
+
alvo
|
| 142 |
+
.iter()
|
| 143 |
+
.map(|&a| {
|
| 144 |
+
let ruido = rng.normal(0.0, 1.0);
|
| 145 |
+
(a + ruido - a).abs()
|
| 146 |
+
})
|
| 147 |
+
.sum::<f64>()
|
| 148 |
+
})
|
| 149 |
+
.collect();
|
| 150 |
+
let media: Vec<f64> = (0..4)
|
| 151 |
+
.map(|j| {
|
| 152 |
+
(0..n).map(|i| alvo[j] + rng.normal(0.0, 1.0)).sum::<f64>() / n as f64
|
| 153 |
+
})
|
| 154 |
+
.collect();
|
| 155 |
+
let erro_media: f64 = media.iter().zip(alvo.iter()).map(|(m, a)| (m - a).abs()).sum();
|
| 156 |
+
let mut ordenado = erros.clone();
|
| 157 |
+
ordenado.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
| 158 |
+
let mediana = ordenado[n / 2];
|
| 159 |
+
if erro_media <= mediana {
|
| 160 |
+
venceu += 1;
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
// a média vence a mediana na grande maioria dos casos (E[erro da
|
| 164 |
+
// média] = σ/√n < σ = E[erro de UMA tentativa])
|
| 165 |
+
assert!(
|
| 166 |
+
venceu as f64 / reps as f64 >= 0.6,
|
| 167 |
+
"auto-consistência venceu só {}/{}",
|
| 168 |
+
venceu,
|
| 169 |
+
reps
|
| 170 |
+
);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// ------------------------------------------------------------------
|
| 174 |
+
// Quebra-cabeça (v0.7.0) — gates Q-1..Q-6 (provas r-31)
|
| 175 |
+
// ------------------------------------------------------------------
|
| 176 |
+
|
| 177 |
+
fn cenario_quebra_cabeca()
|
| 178 |
+
-> (Tensor, crate::verificador::VerificadorNcm, crate::arvore_pesos::ArvorePesos) {
|
| 179 |
+
let dev = Device::Cpu;
|
| 180 |
+
let mut rng = RngDeterministico::novo(777);
|
| 181 |
+
// "conhecimento treinado": 12 conceitos (d=8) com estrutura
|
| 182 |
+
let m = 12usize;
|
| 183 |
+
let d = 8usize;
|
| 184 |
+
let v: Vec<f32> = (0..m * d).map(|i| rng.normal_padrao() as f32).collect();
|
| 185 |
+
let conceitos = Tensor::from_vec(v, (m, d), &dev).unwrap();
|
| 186 |
+
// verificador calibrado com um "verificador" sintético: logits
|
| 187 |
+
// favoráveis ao agregado médio dos conceitos
|
| 188 |
+
let mut verif = crate::verificador::VerificadorNcm::novo();
|
| 189 |
+
let _ = &mut verif;
|
| 190 |
+
let mut arvore = crate::arvore_pesos::ArvorePesos::nova(&std::collections::HashMap::new());
|
| 191 |
+
let _ = &mut arvore;
|
| 192 |
+
(conceitos, verif, arvore)
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
fn logits_fn_exemplo(x: &Tensor) -> Result<Tensor> {
|
| 196 |
+
// logits determinísticos correlacionados com a norma do agregado —
|
| 197 |
+
// quanto mais "concentrado" o agregado, maior a confiança
|
| 198 |
+
let norma = x.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
|
| 199 |
+
let base = (norma * 2.0).min(8.0);
|
| 200 |
+
let v: Vec<f32> = vec![base as f32, 0.5, 0.2, 0.1];
|
| 201 |
+
Ok(Tensor::from_vec(v, (4,), x.device())?)
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
/// Q-2/Q-3: todo ACEITO tem conf ≥ τ_c; rejeições não aplicam delta;
|
| 205 |
+
/// potencial monotônico (confiança acumulada só cresce nos aceites).
|
| 206 |
+
#[test]
|
| 207 |
+
fn quebra_cabeca_soundness_e_potencial() -> Result<()> {
|
| 208 |
+
let (conceitos, verif, mut arvore) = cenario_quebra_cabeca();
|
| 209 |
+
let cfg = ConfigQuebraCabeca::novo(4, 10, 0.55);
|
| 210 |
+
let mut rng = RngDeterministico::novo(4242);
|
| 211 |
+
let r = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?;
|
| 212 |
+
for rod in &r.rodadas {
|
| 213 |
+
if rod.aceito {
|
| 214 |
+
assert!(
|
| 215 |
+
rod.confianca >= cfg.tau_c,
|
| 216 |
+
"Q-2: aceito com conf {} < τ_c {}",
|
| 217 |
+
rod.confianca,
|
| 218 |
+
cfg.tau_c
|
| 219 |
+
);
|
| 220 |
+
assert!(rod.delta_aplicado > 0.0, "aceite aplica delta > 0");
|
| 221 |
+
} else {
|
| 222 |
+
assert_eq!(rod.delta_aplicado, 0.0, "Q-3: rejeitado não aplica delta");
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
// potencial monotônico: Σ conf dos aceitos ≤ Σ max(conf, τ_c) das rodadas
|
| 226 |
+
let pot: f64 = r.rodadas.iter().filter(|x| x.aceito).map(|x| x.confianca).sum();
|
| 227 |
+
assert!(pot >= 0.0);
|
| 228 |
+
// Q-5: invariantes da árvore preservadas
|
| 229 |
+
arvore.invariantes_ok().expect("Q-5: invariantes T-A1 preservadas");
|
| 230 |
+
Ok(())
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
/// Q-4: terminação — nunca excede n_rodadas_max nem roda sem parar.
|
| 234 |
+
#[test]
|
| 235 |
+
fn quebra_cabeca_termina() -> Result<()> {
|
| 236 |
+
let (conceitos, verif, mut arvore) = cenario_quebra_cabeca();
|
| 237 |
+
let cfg = ConfigQuebraCabeca::novo(3, 6, 0.99); // τ inalcançável
|
| 238 |
+
let mut rng = RngDeterministico::novo(1);
|
| 239 |
+
let r = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?;
|
| 240 |
+
assert!(r.rodadas.len() <= 6, "Q-4: orçamento respeitado");
|
| 241 |
+
assert_eq!(r.aceitos, 0, "τ inalcançável ⇒ nenhum aceite");
|
| 242 |
+
assert!(r.rejeitados > 0);
|
| 243 |
+
Ok(())
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
/// Q-4b: rejeições incrementam a PUNIÇÃO no nó pensamento (sinal FNH).
|
| 247 |
+
#[test]
|
| 248 |
+
fn quebra_cabeca_punicao_sobe() -> Result<()> {
|
| 249 |
+
let (conceitos, verif, mut arvore) = cenario_quebra_cabeca();
|
| 250 |
+
let no_pens = arvore.no("pensamento").unwrap();
|
| 251 |
+
let antes = arvore.nos[no_pens].metricas.get("punicao").copied().unwrap_or(0.0);
|
| 252 |
+
let cfg = ConfigQuebraCabeca::novo(3, 5, 0.999);
|
| 253 |
+
let mut rng = RngDeterministico::novo(9);
|
| 254 |
+
let _ = quebra_cabeca_inovar(&conceitos, &verif, &logits_fn_exemplo, &mut arvore, &cfg, &mut rng)?;
|
| 255 |
+
let depois = arvore.nos[no_pens].metricas.get("punicao").copied().unwrap_or(0.0);
|
| 256 |
+
assert!(depois > antes, "punição {depois} deve > {antes} após rejeições");
|
| 257 |
+
Ok(())
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
/// Rotação/ortogonalidade: ‖xQ‖_F = ‖x‖_F (preserva energia) e a
|
| 261 |
+
/// remontagem com rotação difere da sem rotação (exploração real).
|
| 262 |
+
#[test]
|
| 263 |
+
fn quebra_cabeca_rotacao_preserva_norma() -> Result<()> {
|
| 264 |
+
let dev = Device::Cpu;
|
| 265 |
+
let mut rng = RngDeterministico::novo(31);
|
| 266 |
+
let v: Vec<f32> = (0..5 * 6).map(|_| rng.normal_padrao() as f32).collect();
|
| 267 |
+
let x = Tensor::from_vec(v, (5, 6), &dev)?;
|
| 268 |
+
let q = q_ortogonal_deterministica(6, &mut rng, &dev)?;
|
| 269 |
+
let xq = x.matmul(&q)?;
|
| 270 |
+
let nx = x.sqr()?.sum_all()?.to_scalar::<f32>()?;
|
| 271 |
+
let nxq = xq.sqr()?.sum_all()?.to_scalar::<f32>()?;
|
| 272 |
+
assert!(
|
| 273 |
+
(nx - nxq).abs() / nx < 1e-4,
|
| 274 |
+
"‖xQ‖_F = {nxq} deve ≈ ‖x‖_F = {nx} (ortogonalidade)"
|
| 275 |
+
);
|
| 276 |
+
// Q é ortogonal exato
|
| 277 |
+
let delta = crate::stiefel::erro_ortogonalidade(&q)?;
|
| 278 |
+
assert!(delta < 1e-6, "δ_ort(Q) = {delta}");
|
| 279 |
+
// remontagem COM rotação difere da SEM rotação
|
| 280 |
+
let d2 = (&xq - &x)?.abs()?.max_all()?.to_scalar::<f32>()?;
|
| 281 |
+
assert!(d2 > 1e-3, "rotação deve explorar (dif = {d2})");
|
| 282 |
+
Ok(())
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
/// Determinismo: mesma semente ⇒ mesmas rodadas (reprodutibilidade).
|
| 286 |
+
#[test]
|
| 287 |
+
fn quebra_cabeca_deterministico() -> Result<()> {
|
| 288 |
+
let (c1, v1, mut a1) = cenario_quebra_cabeca();
|
| 289 |
+
let (_, v2, mut a2) = cenario_quebra_cabeca();
|
| 290 |
+
let cfg = ConfigQuebraCabeca::novo(4, 8, 0.5);
|
| 291 |
+
let mut r1 = RngDeterministico::novo(123);
|
| 292 |
+
let mut r2 = RngDeterministico::novo(123);
|
| 293 |
+
let q1 = quebra_cabeca_inovar(&c1, &v1, &logits_fn_exemplo, &mut a1, &cfg, &mut r1)?;
|
| 294 |
+
let q2 = quebra_cabeca_inovar(&c1, &v2, &logits_fn_exemplo, &mut a2, &cfg, &mut r2)?;
|
| 295 |
+
assert_eq!(q1.rodadas.len(), q2.rodadas.len());
|
| 296 |
+
for (x, y) in q1.rodadas.iter().zip(q2.rodadas.iter()) {
|
| 297 |
+
assert_eq!(x.permutacao_pecas, y.permutacao_pecas, "mesma semente ⇒ mesma remontagem");
|
| 298 |
+
assert!((x.confianca - y.confianca).abs() < 1e-12);
|
| 299 |
+
assert_eq!(x.aceito, y.aceito);
|
| 300 |
+
}
|
| 301 |
+
Ok(())
|
| 302 |
+
}
|
| 303 |
+
}
|
src/python.rs
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/python/calibracao.rs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `python::calibracao` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
/// **V-1..V-3:** calibra o verificador NCM em logits retidos e devolve
|
| 8 |
+
/// temperatura, ECE antes/depois, NLL antes/depois e o τ_c por quantil DKW.
|
| 9 |
+
#[pyfunction]
|
| 10 |
+
pub(crate) fn calibrar_verificador(
|
| 11 |
+
logits: Vec<Vec<f32>>,
|
| 12 |
+
alvos: Vec<usize>,
|
| 13 |
+
epsilon: f64,
|
| 14 |
+
) -> PyResult<PyObject> {
|
| 15 |
+
Python::with_gil(|py| {
|
| 16 |
+
let dev = candle_core::Device::Cpu;
|
| 17 |
+
let (n, k) = (logits.len(), logits.first().map(|l| l.len()).unwrap_or(1));
|
| 18 |
+
let flat: Vec<f32> = logits.into_iter().flatten().collect();
|
| 19 |
+
let lt = candle_core::Tensor::from_vec(flat, (n, k), &dev)
|
| 20 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 21 |
+
let at = candle_core::Tensor::from_vec(
|
| 22 |
+
alvos.iter().map(|&a| a as u32).collect::<Vec<u32>>(), (n,), &dev)
|
| 23 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 24 |
+
let mut v = verificador::VerificadorNcm::novo();
|
| 25 |
+
let (e_antes, e_depois) = v.ajustar(<, &at)
|
| 26 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 27 |
+
let confs: Vec<f64> = verificador::confianca_por_linha(<)
|
| 28 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?
|
| 29 |
+
.flatten_all().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?
|
| 30 |
+
.to_vec1::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?
|
| 31 |
+
.iter().map(|x| *x as f64).collect();
|
| 32 |
+
let tau_c = verificador::limiar_por_erro(&confs, epsilon);
|
| 33 |
+
let d_ = serde_json::json!({
|
| 34 |
+
"temperatura": v.temperatura, "ece_antes": e_antes, "ece_depois": e_depois,
|
| 35 |
+
"nll_antes": v.n_ll_antes, "nll_depois": v.n_ll_depois,
|
| 36 |
+
"tau_c": tau_c, "epsilon": epsilon, "n": n,
|
| 37 |
+
});
|
| 38 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 39 |
+
})
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
/// **Demo do verificador NCM substituindo o oráculo** (r-21 §3.1): treina
|
| 44 |
+
/// um classificador pequeno, calibra T* em dados retidos, mede ECE, τ_c
|
| 45 |
+
/// (DKW) e a soundness empírica `P(erro|aceito) ≤ 1−τ_c+ECE`; compara com
|
| 46 |
+
/// o ORÁCULO (que usa o alvo — impossível em produção) no mesmo problema.
|
| 47 |
+
#[pyfunction]
|
| 48 |
+
pub(crate) fn demo_verificador_ncm(n: usize, k: usize, semente: u64) -> PyResult<PyObject> {
|
| 49 |
+
Python::with_gil(|py| {
|
| 50 |
+
let dev = candle_core::Device::Cpu;
|
| 51 |
+
crate::rng::definir_semente(semente);
|
| 52 |
+
let mut rng = crate::rng::RngDeterministico::novo(semente);
|
| 53 |
+
// dados sintéticos com DRIFT (treino ≠ consulta — força OOD leve)
|
| 54 |
+
let gerar = |rng: &mut crate::rng::RngDeterministico, m: usize, desloc: f64|
|
| 55 |
+
-> (candle_core::Tensor, Vec<usize>) {
|
| 56 |
+
let mut l = Vec::with_capacity(m * k);
|
| 57 |
+
let mut a = Vec::with_capacity(m);
|
| 58 |
+
for _ in 0..m {
|
| 59 |
+
let alvo = rng.inteiro(k);
|
| 60 |
+
a.push(alvo);
|
| 61 |
+
for j in 0..k {
|
| 62 |
+
let base = if j == alvo { 2.5 } else { 0.0 };
|
| 63 |
+
l.push((base + rng.normal(0.0, 1.2) as f32 + desloc as f32) as f32);
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
let lt = candle_core::Tensor::from_vec(l, (m, k), &dev).unwrap();
|
| 67 |
+
(lt, a)
|
| 68 |
+
};
|
| 69 |
+
let (lt_tre, alvos_tre) = gerar(&mut rng, n, 0.0);
|
| 70 |
+
let (lt_val, alvos_val) = gerar(&mut rng, n / 2, 0.3);
|
| 71 |
+
// mini-classificador: matriz W (k, k) treinada por softmax-CE
|
| 72 |
+
let w = candle_core::Tensor::randn(0f32, 0.1, (k, k), &dev)
|
| 73 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 74 |
+
let w_var = candle_core::Var::from_tensor(&w)
|
| 75 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 76 |
+
let at_t = candle_core::Tensor::from_vec(
|
| 77 |
+
alvos_tre.iter().map(|&a| a as u32).collect::<Vec<u32>>(), (n,), &dev)
|
| 78 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 79 |
+
let mut otim = treino::AdamWManual::novo(vec![w_var.clone()], 0.05, 0.0);
|
| 80 |
+
for _ in 0..120 {
|
| 81 |
+
let logits = lt_tre.matmul(&w_var.as_tensor().transpose(0, 1).unwrap()).unwrap();
|
| 82 |
+
let ce = candle_nn::loss::cross_entropy(&logits, &at_t).unwrap();
|
| 83 |
+
let g = treino::grads_achatados(&ce, &[w_var.clone()]).unwrap();
|
| 84 |
+
otim.passo(g);
|
| 85 |
+
}
|
| 86 |
+
// logits de validação com o modelo treinado
|
| 87 |
+
let logits_val = lt_val.matmul(&w_var.as_tensor().transpose(0, 1).unwrap()).unwrap();
|
| 88 |
+
let at_v = candle_core::Tensor::from_vec(
|
| 89 |
+
alvos_val.iter().map(|&a| a as u32).collect::<Vec<u32>>(), (n / 2,), &dev)
|
| 90 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 91 |
+
// calibração (V-1) + soundness (V-2) + quantil (V-3)
|
| 92 |
+
let mut v = verificador::VerificadorNcm::novo();
|
| 93 |
+
let (ece_antes, ece_depois) = v.ajustar(&logits_val, &at_v)
|
| 94 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 95 |
+
let confs: Vec<f64> = v.confianca(&logits_val).unwrap()
|
| 96 |
+
.flatten_all().unwrap().to_vec1::<f32>().unwrap().iter().map(|x| *x as f64).collect();
|
| 97 |
+
let corretos: Vec<bool> = logits_val.argmax(candle_core::D::Minus1).unwrap()
|
| 98 |
+
.eq(&at_v).unwrap().to_dtype(candle_core::DType::U32).unwrap()
|
| 99 |
+
.flatten_all().unwrap().to_vec1::<u32>().unwrap()
|
| 100 |
+
.iter().map(|x| *x == 1).collect();
|
| 101 |
+
let tau_c = verificador::limiar_por_erro(&confs, 0.25);
|
| 102 |
+
let (mut erro_aceito, mut n_aceito) = (0usize, 0usize);
|
| 103 |
+
for (c, ok) in confs.iter().zip(corretos.iter()) {
|
| 104 |
+
if *c >= tau_c {
|
| 105 |
+
n_aceito += 1;
|
| 106 |
+
if !ok {
|
| 107 |
+
erro_aceito += 1;
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
let p_erro = erro_aceito as f64 / n_aceito.max(1) as f64;
|
| 112 |
+
let cota = (1.0 - tau_c) + ece_depois + 0.06;
|
| 113 |
+
// ORÁCULO no mesmo problema (usa o alvo — só para contraste)
|
| 114 |
+
let acerto_oraculo = corretos.iter().filter(|c| **c).count() as f64 / corretos.len() as f64;
|
| 115 |
+
let d_ = serde_json::json!({
|
| 116 |
+
"versao": verificador::versao(), "n": n, "k": k,
|
| 117 |
+
"temperatura_calibrada": v.temperatura,
|
| 118 |
+
"ece_antes": ece_antes, "ece_depois": ece_depois,
|
| 119 |
+
"nll_antes": v.n_ll_antes, "nll_depois": v.n_ll_depois,
|
| 120 |
+
"tau_c_dkw": tau_c,
|
| 121 |
+
"p_erro_aceito": p_erro, "cota_soundness": cota,
|
| 122 |
+
"soundness_ok": p_erro <= cota,
|
| 123 |
+
"calibracao_melhorou": ece_depois < ece_antes,
|
| 124 |
+
"acerto_oraculo_referencia": acerto_oraculo,
|
| 125 |
+
"verificador_sem_oraculo": true,
|
| 126 |
+
"gates": {
|
| 127 |
+
"gate_v1_calibracao": ece_depois < ece_antes,
|
| 128 |
+
"gate_v2_soundness": p_erro <= cota,
|
| 129 |
+
"gate_v3_tau_positivo": tau_c > 0.0 && tau_c < 1.0,
|
| 130 |
+
},
|
| 131 |
+
});
|
| 132 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 133 |
+
})
|
| 134 |
+
}
|
src/python/demos_qwen.rs
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `python::demos_qwen` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
/// **v0.8.0 — análise/prova/teste das 4 propostas Qwen3.8** (r-33): mede
|
| 8 |
+
/// custo e exatidão de (1) Newton analítico vs golden-section (verificador),
|
| 9 |
+
/// (2) Cayley vs polar (FNH), (3) árvore pós-ordem iterativa vs recursiva,
|
| 10 |
+
/// (4) MoE vetorizada vs referência (valores + gradiente restaurado — bug
|
| 11 |
+
/// v080-1), e re-executa a ablação JEPA de 3 braços com a correção.
|
| 12 |
+
#[pyfunction]
|
| 13 |
+
#[allow(deprecated)] // mede o bug v080-1 intencionalmente (fwd = oráculo)
|
| 14 |
+
pub(crate) fn demo_qwen_v080(py: Python) -> PyResult<PyObject> {
|
| 15 |
+
use candle_core::{Device, Tensor};
|
| 16 |
+
use std::time::Instant;
|
| 17 |
+
let dev = Device::Cpu;
|
| 18 |
+
|
| 19 |
+
// ---------- (1) Newton vs golden-section (verificador) ----------
|
| 20 |
+
let mut rng = crate::rng::RngDeterministico::novo(808);
|
| 21 |
+
let (n, k) = (1024usize, 32usize);
|
| 22 |
+
let mut lv = Vec::with_capacity(n * k);
|
| 23 |
+
let mut av = Vec::with_capacity(n);
|
| 24 |
+
for _ in 0..n {
|
| 25 |
+
let alvo = rng.inteiro(k) as u32;
|
| 26 |
+
av.push(alvo);
|
| 27 |
+
for j in 0..k {
|
| 28 |
+
let base = if j as u32 == alvo { 3.0 } else { 0.0 };
|
| 29 |
+
lv.push(base + rng.normal(0.0, 1.0) as f32);
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
let lt = Tensor::from_vec(lv, (n, k), &dev).unwrap();
|
| 33 |
+
let at = Tensor::from_vec(av, (n,), &dev).unwrap();
|
| 34 |
+
let t0 = Instant::now();
|
| 35 |
+
let (t_g, _, nll_g) = verificador::calibrar_temperatura(<, &at).unwrap();
|
| 36 |
+
let tempo_golden = t0.elapsed().as_secs_f64();
|
| 37 |
+
let t0 = Instant::now();
|
| 38 |
+
let (t_n, _, nll_n, n_aval) =
|
| 39 |
+
verificador::calibrar_temperatura_newton(<, &at, 40).unwrap();
|
| 40 |
+
let tempo_newton = t0.elapsed().as_secs_f64();
|
| 41 |
+
|
| 42 |
+
// ---------- (2) Cayley vs polar (FNH) ----------
|
| 43 |
+
let n_rot = 16usize;
|
| 44 |
+
let r = fnh::RotacaoSkew::nova(n_rot, &dev).unwrap();
|
| 45 |
+
let th: Vec<f32> = (0..n_rot * (n_rot - 1) / 2)
|
| 46 |
+
.map(|i| ((i as f32) * 0.11) % 0.5)
|
| 47 |
+
.collect();
|
| 48 |
+
r.theta
|
| 49 |
+
.set(&Tensor::from_vec(th, (n_rot * (n_rot - 1) / 2,), &dev).unwrap())
|
| 50 |
+
.unwrap();
|
| 51 |
+
let rep = 25usize;
|
| 52 |
+
let t0 = Instant::now();
|
| 53 |
+
let mut delta_polar = 0.0;
|
| 54 |
+
for _ in 0..rep {
|
| 55 |
+
delta_polar = crate::stiefel::erro_ortogonalidade(&r.rotacao_polar().unwrap()).unwrap();
|
| 56 |
+
}
|
| 57 |
+
let tempo_polar = t0.elapsed().as_secs_f64() / rep as f64;
|
| 58 |
+
let t0 = Instant::now();
|
| 59 |
+
let mut delta_cayley = 0.0;
|
| 60 |
+
for _ in 0..rep {
|
| 61 |
+
delta_cayley = crate::stiefel::erro_ortogonalidade(&r.rotacao_cayley().unwrap()).unwrap();
|
| 62 |
+
}
|
| 63 |
+
let tempo_cayley = t0.elapsed().as_secs_f64() / rep as f64;
|
| 64 |
+
|
| 65 |
+
// ---------- (3) árvore iterativa vs recursiva ----------
|
| 66 |
+
let mut arv = arvore_pesos::ArvorePesos::nova(&std::collections::HashMap::new());
|
| 67 |
+
let mut pai = 1usize;
|
| 68 |
+
for i in 0..20_000u32 {
|
| 69 |
+
pai = arv.anexar_filho(pai, &format!("c{i}"), 1);
|
| 70 |
+
}
|
| 71 |
+
arv.nos[pai].metricas.insert("m".into(), 1.0);
|
| 72 |
+
let t0 = Instant::now();
|
| 73 |
+
let total_it = arv.agregar_fundo_topo("m");
|
| 74 |
+
let tempo_arv_it = t0.elapsed().as_secs_f64();
|
| 75 |
+
// repõe TODAS as métricas locais (a agregação sobrescreveu com os
|
| 76 |
+
// agregados) — só a folha tem valor; o resto volta a 0 — e roda a
|
| 77 |
+
// recursiva (profundidade 20k ok na thread principal do Python)
|
| 78 |
+
for no in arv.nos.iter_mut() {
|
| 79 |
+
no.metricas.insert("m".into(), 0.0);
|
| 80 |
+
}
|
| 81 |
+
arv.nos[pai].metricas.insert("m".into(), 1.0);
|
| 82 |
+
let t0 = Instant::now();
|
| 83 |
+
let total_rec = arv.agregar_fundo_topo_rec("m");
|
| 84 |
+
let tempo_arv_rec = t0.elapsed().as_secs_f64();
|
| 85 |
+
|
| 86 |
+
// ---------- (4) MoE vetorizada: valores + gradiente + custo ----------
|
| 87 |
+
let (nm, dm, em) = (256usize, 32usize, 8usize);
|
| 88 |
+
let xv: Vec<f32> = (0..nm * dm).map(|_| rng.normal(0.0, 1.0) as f32).collect();
|
| 89 |
+
let x = Tensor::from_vec(xv, (nm, dm), &dev).unwrap();
|
| 90 |
+
let moe = moe_redirecionavel::MoeRedirecionavel::novo(dm, em, 2, nm, &dev).unwrap();
|
| 91 |
+
let s_ref = moe.fwd(&x, moe_redirecionavel::Papel::Codificador).unwrap();
|
| 92 |
+
let s_vet = moe.forward(&x, moe_redirecionavel::Papel::Codificador).unwrap();
|
| 93 |
+
let dif_moe = (&s_ref.saida - &s_vet.saida)
|
| 94 |
+
.unwrap()
|
| 95 |
+
.abs()
|
| 96 |
+
.unwrap()
|
| 97 |
+
.max_all()
|
| 98 |
+
.unwrap()
|
| 99 |
+
.to_scalar::<f32>()
|
| 100 |
+
.unwrap() as f64;
|
| 101 |
+
let rep_moe = 10usize;
|
| 102 |
+
let t0 = Instant::now();
|
| 103 |
+
for _ in 0..rep_moe {
|
| 104 |
+
let _ = moe.fwd(&x, moe_redirecionavel::Papel::Codificador).unwrap();
|
| 105 |
+
}
|
| 106 |
+
let tempo_moe_ref = t0.elapsed().as_secs_f64() / rep_moe as f64;
|
| 107 |
+
let t0 = Instant::now();
|
| 108 |
+
for _ in 0..rep_moe {
|
| 109 |
+
let _ = moe.forward(&x, moe_redirecionavel::Papel::Codificador).unwrap();
|
| 110 |
+
}
|
| 111 |
+
let tempo_moe_vet = t0.elapsed().as_secs_f64() / rep_moe as f64;
|
| 112 |
+
// normas de gradiente (bug v080-1: fwd = 0; vetorizada > 0)
|
| 113 |
+
let norma = |vet: bool| -> (f64, f64) {
|
| 114 |
+
let s = if vet {
|
| 115 |
+
moe.forward(&x, moe_redirecionavel::Papel::Codificador).unwrap()
|
| 116 |
+
} else {
|
| 117 |
+
moe.fwd(&x, moe_redirecionavel::Papel::Codificador).unwrap()
|
| 118 |
+
};
|
| 119 |
+
let perda = s.saida.sqr().unwrap().sum_all().unwrap();
|
| 120 |
+
let gs = perda.backward().unwrap();
|
| 121 |
+
let mut nr = 0.0;
|
| 122 |
+
for v in moe.roteador.vars() {
|
| 123 |
+
if let Some(g) = gs.get(&v) {
|
| 124 |
+
nr += g.sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap() as f64;
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
let mut ne = 0.0;
|
| 128 |
+
for e in &moe.especialistas {
|
| 129 |
+
for v in e.vars() {
|
| 130 |
+
if let Some(g) = gs.get(&v) {
|
| 131 |
+
ne += g.sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap() as f64;
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
(nr, ne)
|
| 136 |
+
};
|
| 137 |
+
let (grad_ref_r, grad_ref_e) = norma(false);
|
| 138 |
+
let (grad_vet_r, grad_vet_e) = norma(true);
|
| 139 |
+
|
| 140 |
+
// ---------- (5) re-ablação JEPA 3 braços (gradiente restaurado) ----------
|
| 141 |
+
let xv: Vec<f32> = (0..64 * 12).map(|_| rng.normal(0.0, 1.0) as f32).collect();
|
| 142 |
+
let xj = Tensor::from_vec(xv, (64, 12), &dev).unwrap();
|
| 143 |
+
let mut ablac = serde_json::Map::new();
|
| 144 |
+
for modo in ["pequena", "densa", "moe"] {
|
| 145 |
+
let rj = moe_redirecionavel::treinar_jepa_com_moe(&xj, modo, 120, 0.5, 42).unwrap();
|
| 146 |
+
ablac.insert(
|
| 147 |
+
modo.into(),
|
| 148 |
+
serde_json::json!({
|
| 149 |
+
"perda_inicial": rj.perda_pred_inicial,
|
| 150 |
+
"perda_final": rj.perda_pred_final,
|
| 151 |
+
"queda_pct": 100.0 * (1.0 - rj.perda_pred_final / rj.perda_pred_inicial.max(1e-9)),
|
| 152 |
+
}),
|
| 153 |
+
);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
let d_ = serde_json::json!({
|
| 157 |
+
"versao": "qwen-v080-1.0",
|
| 158 |
+
"newton_vs_golden": {
|
| 159 |
+
"t_golden": t_g, "t_newton": t_n, "dif_t": (t_g - t_n).abs(),
|
| 160 |
+
"nll_golden": nll_g, "nll_newton": nll_n,
|
| 161 |
+
"nll_dif": (nll_g - nll_n).abs(),
|
| 162 |
+
"n_aval_newton": n_aval,
|
| 163 |
+
"tempo_golden_s": tempo_golden, "tempo_newton_s": tempo_newton,
|
| 164 |
+
"speedup": tempo_golden / tempo_newton.max(1e-9),
|
| 165 |
+
"gates": {
|
| 166 |
+
"mesmo_t": (t_g - t_n).abs() < 1e-3,
|
| 167 |
+
"nll_nao_piora": nll_n <= nll_g + 1e-4,
|
| 168 |
+
"menos_avaliacoes": n_aval < 36,
|
| 169 |
+
},
|
| 170 |
+
},
|
| 171 |
+
"cayley_vs_polar": {
|
| 172 |
+
"delta_ort_polar": delta_polar, "delta_ort_cayley": delta_cayley,
|
| 173 |
+
"tempo_polar_s": tempo_polar, "tempo_cayley_s": tempo_cayley,
|
| 174 |
+
"speedup": tempo_polar / tempo_cayley.max(1e-9),
|
| 175 |
+
"gates": {
|
| 176 |
+
"ambos_exatos": delta_polar < 1e-6 && delta_cayley < 1e-6,
|
| 177 |
+
"cayley_mais_rapido": tempo_cayley < tempo_polar,
|
| 178 |
+
},
|
| 179 |
+
},
|
| 180 |
+
"arvore_iter_vs_rec": {
|
| 181 |
+
"n_nos": arv.n_nos(),
|
| 182 |
+
"total_iter": total_it, "total_rec": total_rec,
|
| 183 |
+
"bit_a_bit": total_it.to_bits() == total_rec.to_bits(),
|
| 184 |
+
"tempo_iter_s": tempo_arv_it, "tempo_rec_s": tempo_arv_rec,
|
| 185 |
+
"gates": { "iguais_bit_a_bit": total_it == total_rec },
|
| 186 |
+
},
|
| 187 |
+
"moe_vetorizada": {
|
| 188 |
+
"max_dif_valores": dif_moe,
|
| 189 |
+
"tempo_ref_s": tempo_moe_ref, "tempo_vet_s": tempo_moe_vet,
|
| 190 |
+
"grad_roteador_ref": grad_ref_r, "grad_especialistas_ref": grad_ref_e,
|
| 191 |
+
"grad_roteador_vet": grad_vet_r, "grad_especialistas_vet": grad_vet_e,
|
| 192 |
+
"gates": {
|
| 193 |
+
"valores_iguais": dif_moe < 1e-4,
|
| 194 |
+
"bug_v080_1_confirmado": grad_ref_r == 0.0 && grad_ref_e == 0.0,
|
| 195 |
+
"gradiente_restaurado": grad_vet_r > 0.0 && grad_vet_e > 0.0,
|
| 196 |
+
},
|
| 197 |
+
},
|
| 198 |
+
"ablacao_jepa_3_bracos_v080": ablac,
|
| 199 |
+
});
|
| 200 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
/// **v0.9.0 — veredito executável das críticas Qwen3.8 (r-34):** mede os
|
| 205 |
+
/// aprimoramentos contra as referências v0.8.0 (oráculos preservados):
|
| 206 |
+
/// (1) skew O(n²) via index_add — bit-a-bit + tempo + memória analítica;
|
| 207 |
+
/// (2) calibrar vetorial — bit-a-bit em 3 passos EMA + tempo;
|
| 208 |
+
/// (3) forward canônico (ex-fwd_vetorizado) — regressão de valores e
|
| 209 |
+
/// gradiente;
|
| 210 |
+
/// (4) newtypes de domínio — rejeição de fora-de-domínio.
|
| 211 |
+
#[pyfunction]
|
| 212 |
+
pub(crate) fn demo_criticas_v090(py: Python) -> PyResult<PyObject> {
|
| 213 |
+
use candle_core::{Device, Tensor};
|
| 214 |
+
use std::time::Instant;
|
| 215 |
+
let dev = Device::Cpu;
|
| 216 |
+
let mut rng = crate::rng::RngDeterministico::novo(909);
|
| 217 |
+
|
| 218 |
+
// ---------- (1) skew: index_add O(n²) vs matmul de seleção O(n⁴) ----------
|
| 219 |
+
let mut skew_rel = serde_json::Map::new();
|
| 220 |
+
let mut bits_todos = true;
|
| 221 |
+
for &n in &[16usize, 64, 128] {
|
| 222 |
+
let r = fnh::RotacaoSkew::nova(n, &dev).unwrap();
|
| 223 |
+
let m = n * (n - 1) / 2;
|
| 224 |
+
let th: Vec<f32> = (0..m).map(|_| rng.uniforme_intervalo(-0.9, 0.9) as f32).collect();
|
| 225 |
+
r.theta.set(&Tensor::from_vec(th, (m,), &dev).unwrap()).unwrap();
|
| 226 |
+
// bit-a-bit (F-13)
|
| 227 |
+
let an = r.skew().unwrap().flatten_all().unwrap().to_vec1::<f32>().unwrap();
|
| 228 |
+
let ar = r.skew_matmul_referencia().unwrap().flatten_all().unwrap().to_vec1::<f32>().unwrap();
|
| 229 |
+
let bits_iguais = an.iter().zip(ar.iter()).all(|(a, b)| a.to_bits() == b.to_bits());
|
| 230 |
+
bits_todos &= bits_iguais;
|
| 231 |
+
// tempo (referência esquentada 1×; média de 5)
|
| 232 |
+
let _ = r.skew_matmul_referencia().unwrap();
|
| 233 |
+
let t0 = Instant::now();
|
| 234 |
+
for _ in 0..5 { let _ = r.skew_matmul_referencia().unwrap(); }
|
| 235 |
+
let t_ref = t0.elapsed().as_secs_f64() / 5.0;
|
| 236 |
+
let _ = r.skew().unwrap();
|
| 237 |
+
let t0 = Instant::now();
|
| 238 |
+
for _ in 0..5 { let _ = r.skew().unwrap(); }
|
| 239 |
+
let t_novo = t0.elapsed().as_secs_f64() / 5.0;
|
| 240 |
+
let bytes_ref = (m * n * n * 4) as f64;
|
| 241 |
+
let bytes_novo = ((n * n + 3 * m) * 4 + 2 * m * 4) as f64;
|
| 242 |
+
skew_rel.insert(
|
| 243 |
+
format!("n_{n}"),
|
| 244 |
+
serde_json::json!({
|
| 245 |
+
"tempo_ref_s": t_ref, "tempo_novo_s": t_novo,
|
| 246 |
+
"speedup": t_ref / t_novo.max(1e-12),
|
| 247 |
+
"bytes_ref_selecao": bytes_ref, "bytes_novo": bytes_novo,
|
| 248 |
+
"razao_memoria": bytes_ref / bytes_novo,
|
| 249 |
+
"bits_iguais": bits_iguais,
|
| 250 |
+
}),
|
| 251 |
+
);
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
// ---------- (2) calibrar: tensor vs laços nativos ----------
|
| 255 |
+
let (d_out, d_in, n_lin) = (512usize, 512usize, 4096usize);
|
| 256 |
+
let wv: Vec<f32> = (0..d_out * d_in).map(|_| rng.uniforme_intervalo(-2.0, 2.0) as f32).collect();
|
| 257 |
+
let w = Tensor::from_vec(wv, (d_out, d_in), &dev).unwrap();
|
| 258 |
+
let xv: Vec<f32> = (0..n_lin * d_in).map(|_| rng.uniforme_intervalo(-3.0, 3.0) as f32).collect();
|
| 259 |
+
let x = Tensor::from_vec(xv, (n_lin, d_in), &dev).unwrap();
|
| 260 |
+
let mut s_novo = fnh::SuavizadorQuant::novo(0.5, 0.98);
|
| 261 |
+
let mut s_tens = fnh::SuavizadorQuant::novo(0.5, 0.98);
|
| 262 |
+
let mut s_ref = fnh::SuavizadorQuant::novo(0.5, 0.98);
|
| 263 |
+
let mut bits_cal = true;
|
| 264 |
+
for _ in 0..3 {
|
| 265 |
+
s_novo.calibrar(&w, &x).unwrap();
|
| 266 |
+
s_tens.calibrar_tensor(&w, &x).unwrap();
|
| 267 |
+
s_ref.calibrar_referencia(&w, &x).unwrap();
|
| 268 |
+
for (a, b) in s_novo.w_max.as_ref().unwrap().iter().zip(s_ref.w_max.as_ref().unwrap()) {
|
| 269 |
+
bits_cal &= a.to_bits() == b.to_bits();
|
| 270 |
+
}
|
| 271 |
+
for (a, b) in s_novo.x_max.as_ref().unwrap().iter().zip(s_ref.x_max.as_ref().unwrap()) {
|
| 272 |
+
bits_cal &= a.to_bits() == b.to_bits();
|
| 273 |
+
}
|
| 274 |
+
}
|
| 275 |
+
// tempo (esquentadas 1×; média de 5)
|
| 276 |
+
let _ = s_ref.calibrar_referencia(&w, &x);
|
| 277 |
+
let t0 = Instant::now();
|
| 278 |
+
for _ in 0..5 { s_ref.calibrar_referencia(&w, &x).unwrap(); }
|
| 279 |
+
let t_cal_ref = t0.elapsed().as_secs_f64() / 5.0;
|
| 280 |
+
let _ = s_novo.calibrar(&w, &x);
|
| 281 |
+
let t0 = Instant::now();
|
| 282 |
+
for _ in 0..5 { s_novo.calibrar(&w, &x).unwrap(); }
|
| 283 |
+
let t_cal_novo = t0.elapsed().as_secs_f64() / 5.0;
|
| 284 |
+
let _ = s_tens.calibrar_tensor(&w, &x);
|
| 285 |
+
let t0 = Instant::now();
|
| 286 |
+
for _ in 0..5 { s_tens.calibrar_tensor(&w, &x).unwrap(); }
|
| 287 |
+
let t_cal_tens = t0.elapsed().as_secs_f64() / 5.0;
|
| 288 |
+
|
| 289 |
+
// ---------- (3) forward: regressão de valores + gradiente ----------
|
| 290 |
+
let (nm, dm, em) = (256usize, 32usize, 8usize);
|
| 291 |
+
let xv2: Vec<f32> = (0..nm * dm).map(|_| rng.normal(0.0, 1.0) as f32).collect();
|
| 292 |
+
let x2 = Tensor::from_vec(xv2, (nm, dm), &dev).unwrap();
|
| 293 |
+
#[allow(deprecated)] // oráculo do bug v080-1 (r-33 §5)
|
| 294 |
+
let moe = moe_redirecionavel::MoeRedirecionavel::novo(dm, em, 2, nm, &dev).unwrap();
|
| 295 |
+
#[allow(deprecated)]
|
| 296 |
+
let s_refm = moe.fwd(&x2, moe_redirecionavel::Papel::Codificador).unwrap();
|
| 297 |
+
let s_fwd = moe.forward(&x2, moe_redirecionavel::Papel::Codificador).unwrap();
|
| 298 |
+
let dif_moe = (&s_refm.saida - &s_fwd.saida).unwrap().abs().unwrap()
|
| 299 |
+
.max_all().unwrap().to_scalar::<f32>().unwrap() as f64;
|
| 300 |
+
let perda = s_fwd.saida.sqr().unwrap().sum_all().unwrap();
|
| 301 |
+
let gs = perda.backward().unwrap();
|
| 302 |
+
let mut gr = 0.0f64;
|
| 303 |
+
for v in moe.roteador.vars() {
|
| 304 |
+
if let Some(g) = gs.get(&v) {
|
| 305 |
+
gr += g.sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap() as f64;
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
let mut ge = 0.0f64;
|
| 309 |
+
for e in &moe.especialistas {
|
| 310 |
+
for v in e.vars() {
|
| 311 |
+
if let Some(g) = gs.get(&v) {
|
| 312 |
+
ge += g.sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap() as f64;
|
| 313 |
+
}
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
// ---------- (4) newtypes de domínio (T-V1) ----------
|
| 318 |
+
let rejeita = crate::tipos::Probabilidade::novo(7.0).is_err()
|
| 319 |
+
&& crate::tipos::Probabilidade::novo(-0.5).is_err()
|
| 320 |
+
&& crate::tipos::Probabilidade::novo(f64::NAN).is_err()
|
| 321 |
+
&& crate::tipos::GateAtivacao::novo(1.5).is_err();
|
| 322 |
+
let aceita = crate::tipos::Probabilidade::novo(0.37).unwrap().valor() == 0.37;
|
| 323 |
+
let gate_tipado = fnh::GatePunicao::default()
|
| 324 |
+
.valor_tipado(&crate::tipos::Probabilidade::novo(0.9).unwrap())
|
| 325 |
+
.valor();
|
| 326 |
+
|
| 327 |
+
let d_ = serde_json::json!({
|
| 328 |
+
"versao": "criticas-v090-1.0",
|
| 329 |
+
"skew_index_add": {
|
| 330 |
+
"por_n": skew_rel,
|
| 331 |
+
"gates": {
|
| 332 |
+
"bits_iguais_todos_n": bits_todos,
|
| 333 |
+
},
|
| 334 |
+
},
|
| 335 |
+
"calibrar_zero_copia": {
|
| 336 |
+
"bits_iguais_3_passos": bits_cal,
|
| 337 |
+
"tempo_ref_s": t_cal_ref, "tempo_novo_s": t_cal_novo,
|
| 338 |
+
"tempo_tensor_s": t_cal_tens,
|
| 339 |
+
"speedup_zero_copia": t_cal_ref / t_cal_novo.max(1e-12),
|
| 340 |
+
"razao_tensor_vs_zero_copia": t_cal_tens / t_cal_novo.max(1e-12),
|
| 341 |
+
"gates": { "bit_a_bit": bits_cal, "nao_regride": t_cal_novo <= t_cal_ref * 1.05 },
|
| 342 |
+
},
|
| 343 |
+
"forward_canonico": {
|
| 344 |
+
"max_dif_valores_vs_oraculo": dif_moe,
|
| 345 |
+
"grad_roteador": gr, "grad_especialistas": ge,
|
| 346 |
+
"gates": {
|
| 347 |
+
"valores_iguais": dif_moe < 1e-4,
|
| 348 |
+
"gradiente_ativo": gr > 0.0 && ge > 0.0,
|
| 349 |
+
},
|
| 350 |
+
},
|
| 351 |
+
"newtypes_dominio": {
|
| 352 |
+
"rejeita_fora_do_dominio": rejeita,
|
| 353 |
+
"aceita_dentro": aceita,
|
| 354 |
+
"gate_tipado_em_0_9": gate_tipado,
|
| 355 |
+
"gates": { "fronteira_validada": rejeita && aceita },
|
| 356 |
+
},
|
| 357 |
+
});
|
| 358 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 359 |
+
}
|
src/python/fnh_arvore.rs
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `python::fnh_arvore` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
use super::nucleo::ConfigPy;
|
| 7 |
+
use super::treino_fases::{carregar_gerador, carregar_tokenizador};
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
// ---------------------------------------------------------------------------
|
| 12 |
+
// v0.7.0 — bindings dos módulos novos (FNH, árvore, quebra-cabeça, NLP/NLG)
|
| 13 |
+
// ---------------------------------------------------------------------------
|
| 14 |
+
|
| 15 |
+
/// Árvore de pesos indexada (arestas duplas) exposta ao Python.
|
| 16 |
+
#[pyclass(name = "ArvorePesos")]
|
| 17 |
+
pub(crate) struct ArvorePesosPy {
|
| 18 |
+
arvore: arvore_pesos::ArvorePesos,
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
#[pymethods]
|
| 22 |
+
impl ArvorePesosPy {
|
| 23 |
+
#[staticmethod]
|
| 24 |
+
#[pyo3(signature = (contagens=None))]
|
| 25 |
+
fn nova(contagens: Option<std::collections::HashMap<String, usize>>) -> Self {
|
| 26 |
+
let c = contagens.unwrap_or_default();
|
| 27 |
+
ArvorePesosPy { arvore: arvore_pesos::ArvorePesos::nova(&c) }
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
fn invariantes_ok(&self) -> bool {
|
| 31 |
+
self.arvore.invariantes_ok().is_ok()
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
fn agregar(&mut self, metrica: &str) -> f64 {
|
| 35 |
+
self.arvore.agregar_fundo_topo(metrica)
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
fn aplicar_ajuste(
|
| 39 |
+
&mut self,
|
| 40 |
+
no: &str,
|
| 41 |
+
metrica: &str,
|
| 42 |
+
delta: f64,
|
| 43 |
+
delta_max: f64,
|
| 44 |
+
) -> PyResult<f64> {
|
| 45 |
+
let idx = self
|
| 46 |
+
.arvore
|
| 47 |
+
.no(no)
|
| 48 |
+
.ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("nó '{no}' inexistente")))?;
|
| 49 |
+
Ok(self.arvore.aplicar_ajuste_indexado(idx, metrica, delta, delta_max))
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
fn definir_metrica(&mut self, no: &str, metrica: &str, valor: f64) -> PyResult<()> {
|
| 53 |
+
let idx = self
|
| 54 |
+
.arvore
|
| 55 |
+
.no(no)
|
| 56 |
+
.ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("nó '{no}' inexistente")))?;
|
| 57 |
+
self.arvore.nos[idx].metricas.insert(metrica.to_string(), valor);
|
| 58 |
+
Ok(())
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
fn metrica(&self, no: &str, metrica: &str) -> PyResult<f64> {
|
| 62 |
+
let idx = self
|
| 63 |
+
.arvore
|
| 64 |
+
.no(no)
|
| 65 |
+
.ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("nó '{no}' inexistente")))?;
|
| 66 |
+
Ok(self.arvore.nos[idx].metricas.get(metrica).copied().unwrap_or(0.0))
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
fn regular(&mut self, origem: &str, delta: f64, lambda: f64) -> PyResult<()> {
|
| 70 |
+
let idx = self
|
| 71 |
+
.arvore
|
| 72 |
+
.no(origem)
|
| 73 |
+
.ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("nó '{origem}' inexistente")))?;
|
| 74 |
+
self.arvore.regular_topo_fundo(idx, delta, lambda);
|
| 75 |
+
Ok(())
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
fn dump(&self) -> PyResult<PyObject> {
|
| 79 |
+
Python::with_gil(|py| {
|
| 80 |
+
let nos: Vec<serde_json::Value> = self
|
| 81 |
+
.arvore
|
| 82 |
+
.nos
|
| 83 |
+
.iter()
|
| 84 |
+
.map(|no| {
|
| 85 |
+
serde_json::json!({
|
| 86 |
+
"nome": no.nome, "ini": no.ini, "fim": no.fim,
|
| 87 |
+
"profundidade": no.profundidade,
|
| 88 |
+
"metricas": no.metricas,
|
| 89 |
+
})
|
| 90 |
+
})
|
| 91 |
+
.collect();
|
| 92 |
+
let d = serde_json::json!({
|
| 93 |
+
"n_nos": self.arvore.n_nos(),
|
| 94 |
+
"n_params": self.arvore.n_params,
|
| 95 |
+
"invariantes_ok": self.arvore.invariantes_ok().is_ok(),
|
| 96 |
+
"nos": nos,
|
| 97 |
+
});
|
| 98 |
+
Ok(pythonize::pythonize(py, &d)?.into())
|
| 99 |
+
})
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
/// Demo/gate da camada FNH sobre o backbone: mede o efeito do gate de
|
| 105 |
+
/// punição nas rotações aprendíveis (ablação: π baixa vs π alta).
|
| 106 |
+
#[pyfunction]
|
| 107 |
+
#[pyo3(signature = (d_in=8, d_out=6, passos=60, seed=42))]
|
| 108 |
+
pub(crate) fn demo_fnh(py: Python, d_in: usize, d_out: usize, passos: usize, seed: u64) -> PyResult<PyObject> {
|
| 109 |
+
let dev = candle_core::Device::Cpu;
|
| 110 |
+
let mut rng = rng::RngDeterministico::novo(seed);
|
| 111 |
+
let mut camada = fnh::CamadaFNH::nova(d_in, d_out, &dev)
|
| 112 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("fnh: {e}")))?;
|
| 113 |
+
let x: Vec<f32> = (0..4 * d_in).map(|_| rng.normal_padrao() as f32).collect();
|
| 114 |
+
let xt = candle_core::Tensor::from_vec(x, (4, d_in), &dev)
|
| 115 |
+
.map_err(py_err)?;
|
| 116 |
+
camada
|
| 117 |
+
.suavizador
|
| 118 |
+
.calibrar(camada.base.w.as_tensor(), &xt)
|
| 119 |
+
.map_err(py_err)?;
|
| 120 |
+
// treino curto com punição ALTA (gate abre) vs BAIXA (gate fechado)
|
| 121 |
+
let mut resumo = serde_json::Map::new();
|
| 122 |
+
for (rotulo, punicao) in [("baixa", 0.1f64), ("alta", 0.95)] {
|
| 123 |
+
let mut cam = fnh::CamadaFNH::nova(d_in, d_out, &dev).map_err(py_err)?;
|
| 124 |
+
// mesma semente de pesos: copia os valores
|
| 125 |
+
let _ = &mut cam;
|
| 126 |
+
let mut hub = telemetria::TelemetryHub::novo(None);
|
| 127 |
+
let mut perdas = Vec::new();
|
| 128 |
+
for p in 0..passos {
|
| 129 |
+
// ruído no x a cada passo (regime não-estacionário leve)
|
| 130 |
+
let ruido: Vec<f32> = (0..4 * d_in)
|
| 131 |
+
.map(|_| (rng.normal(0.0, 0.05)) as f32)
|
| 132 |
+
.collect();
|
| 133 |
+
let xt2 = (&xt
|
| 134 |
+
+ &candle_core::Tensor::from_vec(ruido, (4, d_in), &dev).map_err(py_err)?)
|
| 135 |
+
.map_err(py_err)?;
|
| 136 |
+
let y = cam.forward(&xt2, punicao).map_err(py_err)?;
|
| 137 |
+
// alvo: saída da camada base (auto-referência: regular para W)
|
| 138 |
+
let z_ref = cam.linear_sem_bias(&xt2, cam.base.w.as_tensor()).map_err(py_err)?;
|
| 139 |
+
let perda = (&y).sub(&z_ref).map_err(py_err)?.sqr().map_err(py_err)?.sum_all().map_err(py_err)?;
|
| 140 |
+
perdas.push(perda.to_scalar::<f32>().map_err(py_err)? as f64);
|
| 141 |
+
let grads = perda.backward().map_err(py_err)?;
|
| 142 |
+
// gradiente descendente manual simples nos θ das rotações
|
| 143 |
+
if let Some(g) = grads.get(&cam.r_wp.theta) {
|
| 144 |
+
let gv = g.flatten_all().map_err(py_err)?.to_vec1::<f32>().map_err(py_err)?;
|
| 145 |
+
let th = cam.r_wp.theta.as_tensor().flatten_all().map_err(py_err)?.to_vec1::<f32>().map_err(py_err)?;
|
| 146 |
+
let novo: Vec<f32> = th.iter().zip(gv.iter()).map(|(t, g_)| t - 0.05 * g_).collect();
|
| 147 |
+
cam.r_wp
|
| 148 |
+
.theta
|
| 149 |
+
.set(&candle_core::Tensor::from_vec(novo, th.len(), &dev).map_err(py_err)?)
|
| 150 |
+
.map_err(py_err)?;
|
| 151 |
+
}
|
| 152 |
+
hub.registrar("fnh_gate", cam.gate_ultimo);
|
| 153 |
+
hub.registrar("fnh_punicao", punicao);
|
| 154 |
+
hub.registrar("perda", perdas[p]);
|
| 155 |
+
let _ = p;
|
| 156 |
+
}
|
| 157 |
+
let theta_norma: f64 = cam
|
| 158 |
+
.r_wp
|
| 159 |
+
.theta
|
| 160 |
+
.as_tensor()
|
| 161 |
+
.flatten_all()
|
| 162 |
+
.map_err(py_err)?
|
| 163 |
+
.to_vec1::<f32>()
|
| 164 |
+
.map_err(py_err)?
|
| 165 |
+
.iter()
|
| 166 |
+
.map(|v| (*v as f64).powi(2))
|
| 167 |
+
.sum::<f64>()
|
| 168 |
+
.sqrt();
|
| 169 |
+
let delta_ort = cam.r_wp.erro_ortogonalidade().map_err(py_err)?;
|
| 170 |
+
resumo.insert(
|
| 171 |
+
format!("punicao_{rotulo}"),
|
| 172 |
+
serde_json::json!({
|
| 173 |
+
"gate_final": cam.gate_ultimo,
|
| 174 |
+
"theta_norma": theta_norma,
|
| 175 |
+
"delta_ort": delta_ort,
|
| 176 |
+
"perda_inicial": perdas[0],
|
| 177 |
+
"perda_final": perdas[perdas.len() - 1],
|
| 178 |
+
}),
|
| 179 |
+
);
|
| 180 |
+
}
|
| 181 |
+
let _ = &mut camada;
|
| 182 |
+
let d = serde_json::json!({
|
| 183 |
+
"versao": "fnh-0.7.0",
|
| 184 |
+
"resumo": resumo,
|
| 185 |
+
});
|
| 186 |
+
Ok(pythonize::pythonize(py, &d)?.into())
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
/// **Quebra-cabeça sobre o conhecimento TREINADO REAL** (script 63):
|
| 191 |
+
/// carrega o tokenizador (codebook do nível `nivel` — o conhecimento
|
| 192 |
+
/// aprendido dos dados reais) e o gerador (a cabeça real como logits_fn
|
| 193 |
+
/// do verificador); executa o protocolo completo de r-30 Q-AD.
|
| 194 |
+
#[pyfunction]
|
| 195 |
+
#[pyo3(signature = (cfg, caminho_tokenizador, caminho_gerador, n_pecas=4, n_rodadas=12, tau_c=0.4, nivel=0, seed=777))]
|
| 196 |
+
pub(crate) fn quebra_cabeca_real(
|
| 197 |
+
cfg: &ConfigPy,
|
| 198 |
+
caminho_tokenizador: &str,
|
| 199 |
+
caminho_gerador: &str,
|
| 200 |
+
n_pecas: usize,
|
| 201 |
+
n_rodadas: usize,
|
| 202 |
+
tau_c: f64,
|
| 203 |
+
nivel: usize,
|
| 204 |
+
seed: u64,
|
| 205 |
+
) -> PyResult<PyObject> {
|
| 206 |
+
Python::with_gil(|py| {
|
| 207 |
+
let dev = candle_core::Device::Cpu;
|
| 208 |
+
crate::rng::definir_semente(seed);
|
| 209 |
+
let mut rng = rng::RngDeterministico::novo(seed);
|
| 210 |
+
let mut tok = tokenizador::TokenizadorConceitos::novo(&cfg.cfg, &dev)
|
| 211 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("tok: {e}")))?;
|
| 212 |
+
carregar_tokenizador(&mut tok, caminho_tokenizador)
|
| 213 |
+
.map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
|
| 214 |
+
let mut ger = gerador::GeradorConceitos::novo(&cfg.cfg, &dev)
|
| 215 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("ger: {e}")))?;
|
| 216 |
+
ger.carregar(caminho_gerador)
|
| 217 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("ger carregar: {e}")))?;
|
| 218 |
+
let nivel = nivel.min(tok.quants.len().saturating_sub(1));
|
| 219 |
+
// conhecimento treinado REAL: codebook Stiefel (K, d_lat)
|
| 220 |
+
let codebook = tok
|
| 221 |
+
.quants[nivel]
|
| 222 |
+
.codebook()
|
| 223 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("codebook: {e}")))?;
|
| 224 |
+
let d_lat = codebook.dims()[1];
|
| 225 |
+
let k = ger.k;
|
| 226 |
+
// δ_ort REAL do codebook treinado (rotação Stiefel em treino — A2)
|
| 227 |
+
let delta_ort_codebook = tok.quants[nivel]
|
| 228 |
+
.delta_ort()
|
| 229 |
+
.unwrap_or(1.0);
|
| 230 |
+
// logits_fn REAL: agregado (d_lat,) → proj_entrada → norm → cabeça
|
| 231 |
+
let proj = &ger.proj_entrada;
|
| 232 |
+
let norm = &ger.norm_entrada;
|
| 233 |
+
let cabeca = &ger.cabeca.cabecas[nivel];
|
| 234 |
+
let logits_fn = move |x: &candle_core::Tensor| -> candle_core::Result<candle_core::Tensor> {
|
| 235 |
+
let h = proj.fwd(&x.reshape((1, d_lat))?)?;
|
| 236 |
+
let h = norm.fwd(&h)?;
|
| 237 |
+
let lg = cabeca.fwd(&h)?;
|
| 238 |
+
lg.reshape((k,))
|
| 239 |
+
};
|
| 240 |
+
// verificador com temperatura calibrada pela fase C quando
|
| 241 |
+
// disponível; default 1,0 (a cota V-2 aplica-se com ECE medido)
|
| 242 |
+
let verif = verificador::VerificadorNcm::novo();
|
| 243 |
+
let mut arvore = arvore_pesos::ArvorePesos::nova(&std::collections::HashMap::new());
|
| 244 |
+
let cfg_qc = pensamento::ConfigQuebraCabeca::novo(n_pecas, n_rodadas, tau_c);
|
| 245 |
+
let r = pensamento::quebra_cabeca_inovar(&codebook, &verif, &logits_fn, &mut arvore, &cfg_qc, &mut rng)
|
| 246 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("qc: {e}")))?;
|
| 247 |
+
let rodadas: Vec<serde_json::Value> = r
|
| 248 |
+
.rodadas
|
| 249 |
+
.iter()
|
| 250 |
+
.map(|x| {
|
| 251 |
+
serde_json::json!({
|
| 252 |
+
"rodada": x.rodada, "confianca": x.confianca,
|
| 253 |
+
"aceito": x.aceito, "delta": x.delta_aplicado,
|
| 254 |
+
})
|
| 255 |
+
})
|
| 256 |
+
.collect();
|
| 257 |
+
let d_ = serde_json::json!({
|
| 258 |
+
"versao": "quebra-cabeca-real-0.7.0",
|
| 259 |
+
"nivel": nivel,
|
| 260 |
+
"delta_ort_codebook": delta_ort_codebook,
|
| 261 |
+
"n_conceitos": codebook.dims()[0],
|
| 262 |
+
"d_lat": d_lat,
|
| 263 |
+
"aceitos": r.aceitos,
|
| 264 |
+
"rejeitados": r.rejeitados,
|
| 265 |
+
"confianca_base": r.confianca_base,
|
| 266 |
+
"confianca_melhor": r.confianca_melhor,
|
| 267 |
+
"telemetria": r.telemetria,
|
| 268 |
+
"rodadas": rodadas,
|
| 269 |
+
"arvore_invariantes": arvore.invariantes_ok().is_ok(),
|
| 270 |
+
"punicao_no_pensamento": arvore
|
| 271 |
+
.no("pensamento")
|
| 272 |
+
.and_then(|i| arvore.nos[i].metricas.get("punicao").copied())
|
| 273 |
+
.unwrap_or(0.0),
|
| 274 |
+
});
|
| 275 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 276 |
+
})
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
/// Demo/gate do quebra-cabeça sobre conceitos SINTÉTICOS estruturados
|
| 281 |
+
/// (validação de mecanismo; a avaliação com dados reais vive no script 63).
|
| 282 |
+
#[pyfunction]
|
| 283 |
+
#[pyo3(signature = (m=24, d=8, n_pecas=4, n_rodadas=12, tau_c=0.35, seed=777))]
|
| 284 |
+
pub(crate) fn demo_quebra_cabeca(
|
| 285 |
+
py: Python,
|
| 286 |
+
m: usize,
|
| 287 |
+
d: usize,
|
| 288 |
+
n_pecas: usize,
|
| 289 |
+
n_rodadas: usize,
|
| 290 |
+
tau_c: f64,
|
| 291 |
+
seed: u64,
|
| 292 |
+
) -> PyResult<PyObject> {
|
| 293 |
+
let dev = candle_core::Device::Cpu;
|
| 294 |
+
let mut rng = rng::RngDeterministico::novo(seed);
|
| 295 |
+
// conceitos estruturados: cluster dominante +1 (média direcional) com
|
| 296 |
+
// 25% de outliers fracos — o agregado TEM direção (sem cancelamento)
|
| 297 |
+
let mut v = Vec::with_capacity(m * d);
|
| 298 |
+
for i in 0..m {
|
| 299 |
+
let centro = if i < m / 4 { 0.2 } else { 1.0 };
|
| 300 |
+
for j in 0..d {
|
| 301 |
+
v.push((centro + rng.normal(0.0, 0.25)) as f32);
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
let conceitos = candle_core::Tensor::from_vec(v, (m, d), &dev).map_err(py_err)?;
|
| 305 |
+
// logits_fn ASSIMÉTRICO: projeção fixa W_proj (rotação-sensível — a
|
| 306 |
+
// agregação simétrica seria invariante sob permutação/rotação, e o
|
| 307 |
+
// backbone real é exatamente o avaliador NÃO-invariente da prática).
|
| 308 |
+
let mut wv = Vec::with_capacity(d * 4);
|
| 309 |
+
for _ in 0..d * 4 {
|
| 310 |
+
wv.push((rng.normal_padrao() as f32) * 2.5);
|
| 311 |
+
}
|
| 312 |
+
let w_proj = candle_core::Tensor::from_vec(wv, (d, 4), &dev).map_err(py_err)?;
|
| 313 |
+
// verificador com temperatura padrão
|
| 314 |
+
let verif = verificador::VerificadorNcm::novo();
|
| 315 |
+
let logits_fn = |x: &candle_core::Tensor| -> candle_core::Result<candle_core::Tensor> {
|
| 316 |
+
// logits = xᵀ·W_proj (4,) — sensível à direção do agregado
|
| 317 |
+
x.reshape((1, d))?.matmul(&w_proj)?.reshape((4,))
|
| 318 |
+
};
|
| 319 |
+
let mut arvore = arvore_pesos::ArvorePesos::nova(&std::collections::HashMap::new());
|
| 320 |
+
let cfg = pensamento::ConfigQuebraCabeca::novo(n_pecas, n_rodadas, tau_c);
|
| 321 |
+
let r = pensamento::quebra_cabeca_inovar(&conceitos, &verif, &logits_fn, &mut arvore, &cfg, &mut rng)
|
| 322 |
+
.map_err(py_err)?;
|
| 323 |
+
let rodadas: Vec<serde_json::Value> = r
|
| 324 |
+
.rodadas
|
| 325 |
+
.iter()
|
| 326 |
+
.map(|x| {
|
| 327 |
+
serde_json::json!({
|
| 328 |
+
"rodada": x.rodada, "confianca": x.confianca,
|
| 329 |
+
"aceito": x.aceito, "delta": x.delta_aplicado,
|
| 330 |
+
})
|
| 331 |
+
})
|
| 332 |
+
.collect();
|
| 333 |
+
let d_ = serde_json::json!({
|
| 334 |
+
"versao": "quebra-cabeca-0.7.0",
|
| 335 |
+
"aceitos": r.aceitos,
|
| 336 |
+
"rejeitados": r.rejeitados,
|
| 337 |
+
"confianca_base": r.confianca_base,
|
| 338 |
+
"confianca_melhor": r.confianca_melhor,
|
| 339 |
+
"telemetria": r.telemetria,
|
| 340 |
+
"rodadas": rodadas,
|
| 341 |
+
"arvore_invariantes": arvore.invariantes_ok().is_ok(),
|
| 342 |
+
});
|
| 343 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 344 |
+
}
|
src/python/jepa_treinos.rs
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `python::jepa_treinos` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
/// Treina uma variante JEPA (7 variantes por máscara; lejepa/mcjepa/bjepa
|
| 8 |
+
/// têm funções próprias) sobre patches (n, d) com grade dada.
|
| 9 |
+
#[pyfunction]
|
| 10 |
+
pub(crate) fn treinar_jepa(py: Python, dados: Vec<Vec<f64>>, grid: Vec<usize>, variante: String, cfg: Option<String>) -> PyResult<PyObject> {
|
| 11 |
+
let dev = candle_core::Device::Cpu;
|
| 12 |
+
let t = tensor_de_2d(dados, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 13 |
+
let mut c = jepa::CfgJepa::default();
|
| 14 |
+
if let Some(js) = &cfg {
|
| 15 |
+
if let Ok(v) = serde_json::from_str::<serde_json::Value>(js) {
|
| 16 |
+
if let Some(o) = v.as_object() {
|
| 17 |
+
for (k, val) in o {
|
| 18 |
+
match k.as_str() {
|
| 19 |
+
"d_emb" => c.d_emb = val.as_u64().unwrap_or(32) as usize,
|
| 20 |
+
"epocas" => c.epocas = val.as_u64().unwrap_or(60) as usize,
|
| 21 |
+
"lr" => c.lr = val.as_f64().unwrap_or(3e-3),
|
| 22 |
+
"m" => c.m = val.as_f64().unwrap_or(0.99),
|
| 23 |
+
"lambda_vc" => c.lambda_vc = val.as_f64().unwrap_or(0.5),
|
| 24 |
+
"gamma_vc" => c.gamma_vc = val.as_f64().unwrap_or(1.0),
|
| 25 |
+
"frac_mascara" => c.frac_mascara = val.as_f64().unwrap_or(0.35),
|
| 26 |
+
"semente" => c.semente = val.as_u64().unwrap_or(42),
|
| 27 |
+
_ => {}
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
let var = match variante.as_str() {
|
| 34 |
+
"image" => jepa::VarianteJepa::ImageJepa,
|
| 35 |
+
"vjepa2" => jepa::VarianteJepa::VJepa2,
|
| 36 |
+
"audio" => jepa::VarianteJepa::AudioJepa,
|
| 37 |
+
"point" => jepa::VarianteJepa::PointJepa,
|
| 38 |
+
"jepa3d" => jepa::VarianteJepa::Jepa3D,
|
| 39 |
+
"brain" => jepa::VarianteJepa::BrainJepa,
|
| 40 |
+
"tjepa" => jepa::VarianteJepa::TJepa,
|
| 41 |
+
_ => jepa::VarianteJepa::TJepa,
|
| 42 |
+
};
|
| 43 |
+
let grid2 = if grid.is_empty() { vec![t.dim(0).unwrap_or(1)] } else { grid };
|
| 44 |
+
let rel = jepa::treinar_variante(
|
| 45 |
+
&t,
|
| 46 |
+
&|_n, r| jepa::mascara_por_variante(var, &grid2, c.frac_mascara, r),
|
| 47 |
+
&c,
|
| 48 |
+
var,
|
| 49 |
+
None,
|
| 50 |
+
&dev,
|
| 51 |
+
)
|
| 52 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 53 |
+
let d = serde_json::json!({
|
| 54 |
+
"versao": jepa::versao(),
|
| 55 |
+
"variante": rel.variante,
|
| 56 |
+
"perda_inicial": rel.perda_inicial,
|
| 57 |
+
"perda_final": rel.perda_final,
|
| 58 |
+
"perda_pred_inicial": rel.perda_pred_inicial,
|
| 59 |
+
"perda_pred_final": rel.perda_pred_final,
|
| 60 |
+
"queda_pct": if rel.perda_pred_inicial.is_finite() && rel.perda_pred_inicial > 0.0 {
|
| 61 |
+
100.0 * (1.0 - rel.perda_pred_final / rel.perda_pred_inicial)
|
| 62 |
+
} else { 0.0 },
|
| 63 |
+
"std_emb_inicial": rel.std_emb_inicial,
|
| 64 |
+
"std_emb_final": rel.std_emb_final,
|
| 65 |
+
"colapso_evitado": rel.std_emb_final > 0.05,
|
| 66 |
+
"n_patches": rel.n_patches,
|
| 67 |
+
"epocas": rel.epocas,
|
| 68 |
+
});
|
| 69 |
+
Ok(pythonize::pythonize(py, &d)?.into())
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
/// Treina LeJEPA (edição-condicionado): dados + instruções + editados.
|
| 74 |
+
#[pyfunction]
|
| 75 |
+
pub(crate) fn treinar_lejepa(py: Python, dados: Vec<Vec<f64>>, edits: Vec<Vec<f64>>, editados: Vec<Vec<f64>>, cfg: Option<String>) -> PyResult<PyObject> {
|
| 76 |
+
let dev = candle_core::Device::Cpu;
|
| 77 |
+
let t = tensor_de_2d(dados, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 78 |
+
let e = tensor_de_2d(edits, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 79 |
+
let y = tensor_de_2d(editados, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 80 |
+
let mut c = jepa::CfgJepa::default();
|
| 81 |
+
if let Some(js) = &cfg {
|
| 82 |
+
if let Ok(v) = serde_json::from_str::<serde_json::Value>(js) {
|
| 83 |
+
if let Some(o) = v.as_object() {
|
| 84 |
+
if let Some(x) = o.get("epocas") { c.epocas = x.as_u64().unwrap_or(60) as usize; }
|
| 85 |
+
if let Some(x) = o.get("d_emb") { c.d_emb = x.as_u64().unwrap_or(32) as usize; }
|
| 86 |
+
if let Some(x) = o.get("lr") { c.lr = x.as_f64().unwrap_or(3e-3); }
|
| 87 |
+
if let Some(x) = o.get("semente") { c.semente = x.as_u64().unwrap_or(42); }
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
let rel = jepa::treinar_lejepa(&t, &e, &y, &c, &dev)
|
| 92 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 93 |
+
let d = serde_json::json!({
|
| 94 |
+
"variante": rel.variante, "perda_inicial": rel.perda_inicial,
|
| 95 |
+
"perda_final": rel.perda_final, "std_emb_final": rel.std_emb_final,
|
| 96 |
+
"colapso_evitado": rel.std_emb_final > 0.05,
|
| 97 |
+
});
|
| 98 |
+
Ok(pythonize::pythonize(py, &d)?.into())
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
/// Treina MC-JEPA (cruz-modal): duas modalidades pareadas.
|
| 103 |
+
#[pyfunction]
|
| 104 |
+
pub(crate) fn treinar_mcjepa(py: Python, mod_a: Vec<Vec<f64>>, mod_b: Vec<Vec<f64>>, cfg: Option<String>) -> PyResult<PyObject> {
|
| 105 |
+
let dev = candle_core::Device::Cpu;
|
| 106 |
+
let a = tensor_de_2d(mod_a, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 107 |
+
let b = tensor_de_2d(mod_b, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 108 |
+
let mut c = jepa::CfgJepa::default();
|
| 109 |
+
if let Some(js) = &cfg {
|
| 110 |
+
if let Ok(v) = serde_json::from_str::<serde_json::Value>(js) {
|
| 111 |
+
if let Some(o) = v.as_object() {
|
| 112 |
+
if let Some(x) = o.get("epocas") { c.epocas = x.as_u64().unwrap_or(60) as usize; }
|
| 113 |
+
if let Some(x) = o.get("d_emb") { c.d_emb = x.as_u64().unwrap_or(32) as usize; }
|
| 114 |
+
if let Some(x) = o.get("lr") { c.lr = x.as_f64().unwrap_or(3e-3); }
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
let rel = jepa::treinar_mcjepa(&a, &b, &c, &dev)
|
| 119 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 120 |
+
let d = serde_json::json!({
|
| 121 |
+
"variante": rel.variante, "perda_inicial": rel.perda_inicial,
|
| 122 |
+
"perda_final": rel.perda_final, "std_emb_final": rel.std_emb_final,
|
| 123 |
+
"colapso_evitado": rel.std_emb_final > 0.05,
|
| 124 |
+
});
|
| 125 |
+
Ok(pythonize::pythonize(py, &d)?.into())
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
/// Treina BJEPA (bidirecional com consistência de ciclo) sobre sequência.
|
| 130 |
+
#[pyfunction]
|
| 131 |
+
pub(crate) fn treinar_bjepa(py: Python, serie: Vec<Vec<f64>>, cfg: Option<String>) -> PyResult<PyObject> {
|
| 132 |
+
let dev = candle_core::Device::Cpu;
|
| 133 |
+
let t = tensor_de_2d(serie, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 134 |
+
let mut c = jepa::CfgJepa::default();
|
| 135 |
+
if let Some(js) = &cfg {
|
| 136 |
+
if let Ok(v) = serde_json::from_str::<serde_json::Value>(js) {
|
| 137 |
+
if let Some(o) = v.as_object() {
|
| 138 |
+
if let Some(x) = o.get("epocas") { c.epocas = x.as_u64().unwrap_or(60) as usize; }
|
| 139 |
+
if let Some(x) = o.get("d_emb") { c.d_emb = x.as_u64().unwrap_or(32) as usize; }
|
| 140 |
+
if let Some(x) = o.get("lr") { c.lr = x.as_f64().unwrap_or(3e-3); }
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
let rel = jepa::treinar_bjepa(&t, &c, &dev)
|
| 145 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 146 |
+
let d = serde_json::json!({
|
| 147 |
+
"variante": rel.variante, "perda_inicial": rel.perda_inicial,
|
| 148 |
+
"perda_final": rel.perda_final, "std_emb_final": rel.std_emb_final,
|
| 149 |
+
"colapso_evitado": rel.std_emb_final > 0.05,
|
| 150 |
+
});
|
| 151 |
+
Ok(pythonize::pythonize(py, &d)?.into())
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
/// Demo SonicMoE: roteamento top-k com capacidade e perda de balanceamento.
|
| 156 |
+
#[pyfunction]
|
| 157 |
+
pub(crate) fn demo_sonic_moe(py: Python, n: usize, d: usize, n_expertos: usize, top_k: usize, capacidade: usize) -> PyResult<PyObject> {
|
| 158 |
+
let dev = candle_core::Device::Cpu;
|
| 159 |
+
let moe = jepa::SonicMoe::novo(d, n_expertos, top_k, capacidade, &dev)
|
| 160 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 161 |
+
let x = candle_core::Tensor::randn(0f32, 1.0, (n, d), &dev)
|
| 162 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 163 |
+
let s = moe.fwd(&x).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 164 |
+
let aux = s.perda_aux.to_scalar::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))? as f64;
|
| 165 |
+
let d_ = serde_json::json!({
|
| 166 |
+
"versao": jepa::versao(),
|
| 167 |
+
"carga_por_especialista": s.carga,
|
| 168 |
+
"descartados": s.descartados,
|
| 169 |
+
"perda_aux": aux,
|
| 170 |
+
"capacidade_respeitada": s.carga.iter().all(|&c| c <= capacidade),
|
| 171 |
+
"saida_forma": s.saida.dims().to_vec(),
|
| 172 |
+
});
|
| 173 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
/// Demo ReplaySSM: estabilidade + replay em cenário de troca de tarefa.
|
| 178 |
+
#[pyfunction]
|
| 179 |
+
pub(crate) fn demo_replay_ssm(py: Python, passos: usize, alpha: f64) -> PyResult<PyObject> {
|
| 180 |
+
let dev = candle_core::Device::Cpu;
|
| 181 |
+
let mut ssm = jepa::ReplaySsm::novo(4, 6, alpha, 8, &dev)
|
| 182 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 183 |
+
let mut otim = crate::treino::AdamWManual::novo(ssm.vars(), 1e-2, 0.0);
|
| 184 |
+
let x_antigo = candle_core::Tensor::from_vec(vec![1.0f32; 16 * 4], (16, 4), &dev)
|
| 185 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 186 |
+
let mut hist = Vec::new();
|
| 187 |
+
for _ in 0..passos {
|
| 188 |
+
let h = ssm.scan(&x_antigo).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 189 |
+
let alvo = h.detach();
|
| 190 |
+
let (pt, pn, pr) = ssm.passo_treino(&x_antigo, &alvo, &mut otim)
|
| 191 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 192 |
+
hist.push(serde_json::json!({"total": pt, "novo": pn, "replay": pr}));
|
| 193 |
+
}
|
| 194 |
+
let x_novo = candle_core::Tensor::from_vec(vec![-1.0f32; 16 * 4], (16, 4), &dev)
|
| 195 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 196 |
+
for _ in 0..passos {
|
| 197 |
+
let h = ssm.scan(&x_novo).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 198 |
+
let alvo = h.detach();
|
| 199 |
+
let _ = ssm.passo_treino(&x_novo, &alvo, &mut otim)
|
| 200 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 201 |
+
}
|
| 202 |
+
let h_fin = ssm.scan(&x_antigo).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 203 |
+
let mag = h_fin.sqr().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?
|
| 204 |
+
.max_all().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?
|
| 205 |
+
.to_scalar::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))? as f64;
|
| 206 |
+
let d_ = serde_json::json!({
|
| 207 |
+
"versao": jepa::versao(),
|
| 208 |
+
"alpha_replay": alpha,
|
| 209 |
+
"historico": hist,
|
| 210 |
+
"magnitudes_finitas": mag.is_finite(),
|
| 211 |
+
"magnitude_max_pos_troca": mag,
|
| 212 |
+
});
|
| 213 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
/// Extrai patches 2D de uma imagem achatada (C, H, W) — adaptador Image-JEPA.
|
| 218 |
+
#[pyfunction]
|
| 219 |
+
pub(crate) fn patches_2d(img: Vec<f32>, c: usize, h: usize, w: usize, ph: usize, pw: usize) -> PyResult<Vec<Vec<f32>>> {
|
| 220 |
+
let dev = candle_core::Device::Cpu;
|
| 221 |
+
let t = candle_core::Tensor::from_vec(img, (c, h, w), &dev)
|
| 222 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 223 |
+
let p = jepa::em_patches_2d(&t, ph, pw)
|
| 224 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 225 |
+
p.to_vec2::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
/// Extrai patches canal-independentes de série temporal (C, T) — T-JEPA.
|
| 230 |
+
#[pyfunction]
|
| 231 |
+
pub(crate) fn patches_serie(ts: Vec<f32>, c: usize, t: usize, pt: usize) -> PyResult<Vec<Vec<f32>>> {
|
| 232 |
+
let dev = candle_core::Device::Cpu;
|
| 233 |
+
let tt = candle_core::Tensor::from_vec(ts, (c, t), &dev)
|
| 234 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 235 |
+
let p = jepa::em_patches_serie(&tt, pt)
|
| 236 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 237 |
+
p.to_vec2::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
/// Extrai grupos FPS de nuvem de pontos (n, d) — Point-JEPA.
|
| 242 |
+
#[pyfunction]
|
| 243 |
+
pub(crate) fn grupos_pontos(pc: Vec<Vec<f64>>, k: usize, m: usize, semente: u64) -> PyResult<Vec<Vec<f64>>> {
|
| 244 |
+
let dev = candle_core::Device::Cpu;
|
| 245 |
+
let t = tensor_de_2d(pc, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 246 |
+
let mut rng = crate::rng::RngDeterministico::novo(semente);
|
| 247 |
+
let g = jepa::em_grupos_pontos(&t, k, m, &mut rng)
|
| 248 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 249 |
+
let out = g.to_vec2::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 250 |
+
Ok(out.into_iter().map(|l| l.into_iter().map(|v| v as f64).collect()).collect())
|
| 251 |
+
}
|
src/python/mod.rs
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! FFI Python (PyO3) — dividida por domínio (v0.10.0, r-35 §2).
|
| 2 |
+
|
| 3 |
+
//! FFI Python (PyO3) do KHTST_RUST — todo o `#[pymodule]`, os
|
| 4 |
+
//! `#[pyfunction]`/`#[pyclass]` e a cola Python (conversões,
|
| 5 |
+
//! callbacks, demos) vivem AQUI desde a v0.9.0 (antes: lib.rs,
|
| 6 |
+
//! ~105 kB — crítica "God Object" do agente Qwen3.8, r-34 §5).
|
| 7 |
+
//! A lógica de domínio (src/*.rs) não importa pyo3: `cargo test
|
| 8 |
+
//! --no-default-features` executa a suíte SEM linkar o Python.
|
| 9 |
+
//!
|
| 10 |
+
//! Regra da casa mantida: nenhuma funcionalidade da superfície
|
| 11 |
+
//! Python foi removida — os mesmos símbolos são registrados no
|
| 12 |
+
//! `nucleo_rust`.
|
| 13 |
+
|
| 14 |
+
use pyo3::prelude::*;
|
| 15 |
+
|
| 16 |
+
use crate::*;
|
| 17 |
+
|
| 18 |
+
// ---- divisão SRP v0.10.0 (r-35 §2): submódulos + re-exports ----
|
| 19 |
+
mod calibracao;
|
| 20 |
+
mod demos_qwen;
|
| 21 |
+
mod fnh_arvore;
|
| 22 |
+
mod jepa_treinos;
|
| 23 |
+
mod moe;
|
| 24 |
+
mod nlp_nlg;
|
| 25 |
+
mod nucleo;
|
| 26 |
+
mod pensamento_demos;
|
| 27 |
+
mod som;
|
| 28 |
+
mod treino_fases;
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
// ---- itens mantidos no próprio mod.rs ----
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
/// Adaptador FFI→domínio (v0.9.0): embrulha um callable Python no tipo de
|
| 36 |
+
/// domínio `treino::CallbackTreino` — `treino.rs` não importa pyo3.
|
| 37 |
+
fn callback_treino_de_py(callback: Option<pyo3::Bound<'_, pyo3::PyAny>>) -> treino::CallbackTreino {
|
| 38 |
+
use std::sync::Arc;
|
| 39 |
+
match callback {
|
| 40 |
+
None => None,
|
| 41 |
+
Some(c) => {
|
| 42 |
+
let f: pyo3::Py<pyo3::PyAny> = c.unbind();
|
| 43 |
+
let g: Arc<dyn Fn(&serde_json::Value) + Send + Sync> =
|
| 44 |
+
Arc::new(move |dados: &serde_json::Value| {
|
| 45 |
+
pyo3::Python::with_gil(|py| {
|
| 46 |
+
if let Ok(obj) = pythonize::pythonize(py, dados) {
|
| 47 |
+
let _ = f.call1(py, (obj,));
|
| 48 |
+
}
|
| 49 |
+
});
|
| 50 |
+
});
|
| 51 |
+
Some(g)
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
// ===========================================================================
|
| 58 |
+
// v0.5.0 — suíte SOM 4D, suíte JEPA, Pensamento, telemetria robusta
|
| 59 |
+
// ===========================================================================
|
| 60 |
+
|
| 61 |
+
fn tensor_de_2d(dados: Vec<Vec<f64>>, dev: &candle_core::Device) -> candle_core::Result<candle_core::Tensor> {
|
| 62 |
+
let d = dados.first().map(|r| r.len()).unwrap_or(1).max(1);
|
| 63 |
+
let n = dados.len();
|
| 64 |
+
let flat: Vec<f32> = dados.into_iter().flat_map(|r| r.into_iter().map(|x| x as f32).collect::<Vec<_>>()).collect();
|
| 65 |
+
candle_core::Tensor::from_vec(flat, (n, d), dev)
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
// ---------------------------------------------------------------------------
|
| 70 |
+
// v0.6.0 — verificador NCM (sem oráculo), fase C, MoE redirecionável,
|
| 71 |
+
// acoplador duplex e autoajuste
|
| 72 |
+
// ---------------------------------------------------------------------------
|
| 73 |
+
|
| 74 |
+
/// Converte matriz f64 → Tensor (n, d) F32.
|
| 75 |
+
fn tensor_f64_2d(v: &[Vec<f64>], dev: &candle_core::Device) -> candle_core::Result<candle_core::Tensor> {
|
| 76 |
+
let (n, d) = (v.len(), v.first().map(|l| l.len()).unwrap_or(0));
|
| 77 |
+
candle_core::Tensor::from_vec(v.iter().flatten().cloned().collect(), (n, d), dev)
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
fn py_err(e: candle_core::Error) -> pyo3::PyErr {
|
| 82 |
+
pyo3::exceptions::PyRuntimeError::new_err(format!("{e}"))
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
#[pymodule]
|
| 87 |
+
fn nucleo_rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
| 88 |
+
pyo3_log::init();
|
| 89 |
+
m.add_function(wrap_pyfunction!(nucleo::versao, m)?)?;
|
| 90 |
+
m.add_function(wrap_pyfunction!(nucleo::info_build, m)?)?;
|
| 91 |
+
m.add_function(wrap_pyfunction!(nucleo::lint_config, m)?)?;
|
| 92 |
+
m.add_function(wrap_pyfunction!(treino_fases::treinar_fase_a, m)?)?;
|
| 93 |
+
m.add_function(wrap_pyfunction!(treino_fases::treinar_fase_b, m)?)?;
|
| 94 |
+
m.add_function(wrap_pyfunction!(treino_fases::gerar_amostras, m)?)?;
|
| 95 |
+
m.add_function(wrap_pyfunction!(nucleo::verificar_mascara, m)?)?;
|
| 96 |
+
m.add_function(wrap_pyfunction!(nucleo::verificar_stiefel, m)?)?;
|
| 97 |
+
// v0.10.0 — checkpoints atômicos com prova de integridade
|
| 98 |
+
m.add_function(wrap_pyfunction!(nucleo::checkpoint_valido, m)?)?;
|
| 99 |
+
m.add_function(wrap_pyfunction!(nucleo::checkpoint_manifesto, m)?)?;
|
| 100 |
+
m.add_function(wrap_pyfunction!(nucleo::sha256_arquivo, m)?)?;
|
| 101 |
+
// v0.5.0 — SOM 4D / JEPA / Pensamento
|
| 102 |
+
m.add_function(wrap_pyfunction!(som::treinar_som4d, m)?)?;
|
| 103 |
+
m.add_function(wrap_pyfunction!(som::treinar_gng, m)?)?;
|
| 104 |
+
m.add_function(wrap_pyfunction!(som::treinar_hsom, m)?)?;
|
| 105 |
+
m.add_function(wrap_pyfunction!(som::demo_tsom, m)?)?;
|
| 106 |
+
m.add_function(wrap_pyfunction!(jepa_treinos::treinar_jepa, m)?)?;
|
| 107 |
+
m.add_function(wrap_pyfunction!(jepa_treinos::treinar_lejepa, m)?)?;
|
| 108 |
+
m.add_function(wrap_pyfunction!(jepa_treinos::treinar_mcjepa, m)?)?;
|
| 109 |
+
m.add_function(wrap_pyfunction!(jepa_treinos::treinar_bjepa, m)?)?;
|
| 110 |
+
m.add_function(wrap_pyfunction!(jepa_treinos::demo_sonic_moe, m)?)?;
|
| 111 |
+
m.add_function(wrap_pyfunction!(jepa_treinos::demo_replay_ssm, m)?)?;
|
| 112 |
+
m.add_function(wrap_pyfunction!(pensamento_demos::classificar_erro, m)?)?;
|
| 113 |
+
m.add_function(wrap_pyfunction!(pensamento_demos::confianca_de_logits, m)?)?;
|
| 114 |
+
m.add_function(wrap_pyfunction!(pensamento_demos::projetar_ridge, m)?)?;
|
| 115 |
+
m.add_function(wrap_pyfunction!(pensamento_demos::demo_pensamento, m)?)?;
|
| 116 |
+
m.add_function(wrap_pyfunction!(jepa_treinos::patches_2d, m)?)?;
|
| 117 |
+
m.add_function(wrap_pyfunction!(jepa_treinos::patches_serie, m)?)?;
|
| 118 |
+
m.add_function(wrap_pyfunction!(jepa_treinos::grupos_pontos, m)?)?;
|
| 119 |
+
// v0.6.0
|
| 120 |
+
m.add_function(wrap_pyfunction!(calibracao::calibrar_verificador, m)?)?;
|
| 121 |
+
m.add_function(wrap_pyfunction!(calibracao::demo_verificador_ncm, m)?)?;
|
| 122 |
+
m.add_function(wrap_pyfunction!(treino_fases::treinar_fase_c, m)?)?;
|
| 123 |
+
m.add_function(wrap_pyfunction!(moe::demo_moe_redirecionavel, m)?)?;
|
| 124 |
+
m.add_function(wrap_pyfunction!(moe::jepa_com_moe, m)?)?;
|
| 125 |
+
m.add_function(wrap_pyfunction!(moe::som_com_moe, m)?)?;
|
| 126 |
+
m.add_function(wrap_pyfunction!(moe::demo_acoplador_duplo, m)?)?;
|
| 127 |
+
m.add_function(wrap_pyfunction!(pensamento_demos::demo_pensamento_ncm, m)?)?;
|
| 128 |
+
m.add_function(wrap_pyfunction!(pensamento_demos::demo_autoajuste, m)?)?;
|
| 129 |
+
m.add_function(wrap_pyfunction!(fnh_arvore::demo_fnh, m)?)?;
|
| 130 |
+
m.add_function(wrap_pyfunction!(fnh_arvore::demo_quebra_cabeca, m)?)?;
|
| 131 |
+
m.add_function(wrap_pyfunction!(fnh_arvore::quebra_cabeca_real, m)?)?;
|
| 132 |
+
m.add_function(wrap_pyfunction!(nlp_nlg::demo_nlp_nlg, m)?)?;
|
| 133 |
+
m.add_function(wrap_pyfunction!(som::demo_autoajuste_som, m)?)?;
|
| 134 |
+
// v0.8.0 — análise/prova/teste das propostas Qwen3.8
|
| 135 |
+
m.add_function(wrap_pyfunction!(demos_qwen::demo_qwen_v080, m)?)?;
|
| 136 |
+
// v0.9.0 — veredito executável das críticas Qwen3.8 (r-34)
|
| 137 |
+
m.add_function(wrap_pyfunction!(demos_qwen::demo_criticas_v090, m)?)?;
|
| 138 |
+
m.add_class::<nucleo::BpePy>()?;
|
| 139 |
+
m.add_class::<nucleo::ConfigPy>()?;
|
| 140 |
+
m.add_class::<nucleo::PonteDuplaPy>()?;
|
| 141 |
+
#[cfg(feature = "ponte_async")]
|
| 142 |
+
m.add_class::<nucleo::PonteAsyncPy>()?;
|
| 143 |
+
m.add_class::<fnh_arvore::ArvorePesosPy>()?;
|
| 144 |
+
Ok(())
|
| 145 |
+
}
|
src/python/moe.rs
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `python::moe` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
/// **MoE redirecionável — demo completa (M-R1..M-R5):** neutralidade da
|
| 8 |
+
/// atenção própria (M-R2), invariância de capacidade/perda auxiliar sob
|
| 9 |
+
/// redirecionamento (M-R1), paralelismo medido (M-R3), water-filling por
|
| 10 |
+
/// QE vs uniforme (M-R5) e fechamento da classe (M-R4 — via permutação).
|
| 11 |
+
#[pyfunction]
|
| 12 |
+
pub(crate) fn demo_moe_redirecionavel(d: usize, n: usize, semente: u64) -> PyResult<PyObject> {
|
| 13 |
+
Python::with_gil(|py| {
|
| 14 |
+
let dev = candle_core::Device::Cpu;
|
| 15 |
+
crate::rng::definir_semente(semente);
|
| 16 |
+
let mut rng = crate::rng::RngDeterministico::novo(semente);
|
| 17 |
+
let x: Vec<f32> = (0..n * d).map(|_| rng.normal(0.0, 1.0) as f32).collect();
|
| 18 |
+
let x = candle_core::Tensor::from_vec(x, (n, d), &dev)
|
| 19 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 20 |
+
let mut moe = moe_redirecionavel::MoeRedirecionavel::novo(d, 8, 2, n, &dev)
|
| 21 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 22 |
+
// M-R2: neutralidade no nascimento
|
| 23 |
+
let g0 = moe.atencao.gate.as_tensor().to_scalar::<f32>().unwrap();
|
| 24 |
+
let y0 = moe.atencao.fwd(&x).unwrap();
|
| 25 |
+
let dif0 = (&y0 - &x).unwrap().sqr().unwrap().sum_all().unwrap()
|
| 26 |
+
.to_scalar::<f32>().unwrap() as f64;
|
| 27 |
+
// abre o gate (mecanismo ativo)
|
| 28 |
+
moe.atencao.gate.set(&candle_core::Tensor::new(1.0f32, &dev).unwrap()).unwrap();
|
| 29 |
+
// M-R1: invariância sob redirecionamento
|
| 30 |
+
let s_enc = moe.forward(&x, moe_redirecionavel::Papel::Codificador).unwrap();
|
| 31 |
+
let carga_antes: usize = s_enc.carga.iter().sum();
|
| 32 |
+
let perda_antes = s_enc.perda_aux.to_scalar::<f32>().unwrap() as f64;
|
| 33 |
+
let permuta: Vec<usize> = (0..8).map(|i| (i + 1) % 8).collect();
|
| 34 |
+
moe.redirecionar(&permuta);
|
| 35 |
+
let s_enc2 = moe.forward(&x, moe_redirecionavel::Papel::Codificador).unwrap();
|
| 36 |
+
let carga_depois: usize = s_enc2.carga.iter().sum();
|
| 37 |
+
let perda_depois = s_enc2.perda_aux.to_scalar::<f32>().unwrap() as f64;
|
| 38 |
+
// M-R3: paralelismo medido (enc ∥ dec em 2 threads)
|
| 39 |
+
let (_e, _dc, t_par, t_seq, speedup) = moe.fwd_ambos_par(&x).unwrap();
|
| 40 |
+
// M-R5: water-filling vs uniforme (cota analítica)
|
| 41 |
+
let qes: Vec<f64> = vec![0.15, 0.4, 1.0, 0.25, 0.6, 0.1, 0.35, 0.5];
|
| 42 |
+
let c_wf = moe_redirecionavel::alocar_por_qe(&qes, 16);
|
| 43 |
+
let c_un = vec![2usize; 8];
|
| 44 |
+
let cota = |c: &[usize]| qes.iter().zip(c).map(|(q, ci)| q * q / *ci as f64).sum::<f64>();
|
| 45 |
+
let (cota_wf, cota_un) = (cota(&c_wf), cota(&c_un));
|
| 46 |
+
let d_ = serde_json::json!({
|
| 47 |
+
"versao": moe_redirecionavel::versao(), "d": d, "n": n,
|
| 48 |
+
"valor_gate_no_nascimento": g0, "neutralidade_dif": dif0,
|
| 49 |
+
"carga_antes": carga_antes, "carga_depois": carga_depois,
|
| 50 |
+
"perda_aux_antes": perda_antes, "perda_aux_depois": perda_depois,
|
| 51 |
+
"t_paralelo_s": t_par, "t_sequencial_s": t_seq, "speedup": speedup,
|
| 52 |
+
"alocacao_water_filling": c_wf, "cota_wf": cota_wf, "cota_uniforme": cota_un,
|
| 53 |
+
"gates": {
|
| 54 |
+
"gate_mr1_capacidade": carga_antes == carga_depois,
|
| 55 |
+
"gate_mr1_perda_aux": (perda_antes - perda_depois).abs() < 2.0 + 1e-6,
|
| 56 |
+
"gate_mr2_neutralidade": dif0 == 0.0 && g0 == 0.0,
|
| 57 |
+
"gate_mr3_paralelismo": t_par < t_seq,
|
| 58 |
+
"gate_mr5_water_filling": cota_wf <= cota_un + 1e-9,
|
| 59 |
+
},
|
| 60 |
+
});
|
| 61 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 62 |
+
})
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
/// **JEPA com MoE redirecionável (ablação de 3 braços):** `modo` ∈
|
| 67 |
+
/// {"pequena", "densa", "moe"} — o braço "densa" tem a capacidade TOTAL dos
|
| 68 |
+
/// especialistas SEM roteamento (isola o ganho do roteamento em si).
|
| 69 |
+
#[pyfunction]
|
| 70 |
+
pub(crate) fn jepa_com_moe(
|
| 71 |
+
patches: Vec<Vec<f32>>,
|
| 72 |
+
modo: String,
|
| 73 |
+
passos: usize,
|
| 74 |
+
frac_mascara: f64,
|
| 75 |
+
semente: u64,
|
| 76 |
+
) -> PyResult<PyObject> {
|
| 77 |
+
Python::with_gil(|py| {
|
| 78 |
+
let dev = candle_core::Device::Cpu;
|
| 79 |
+
let t = tensor_f64_2d(&patches.into_iter().map(|l| l.into_iter().map(|x| x as f64).collect()).collect::<Vec<Vec<f64>>>(), &dev)
|
| 80 |
+
.and_then(|t| t.to_dtype(candle_core::DType::F32))
|
| 81 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 82 |
+
let modo: &'static str = match modo.as_str() {
|
| 83 |
+
"pequena" => "pequena",
|
| 84 |
+
"densa" => "densa",
|
| 85 |
+
_ => "moe",
|
| 86 |
+
};
|
| 87 |
+
let r = moe_redirecionavel::treinar_jepa_com_moe(&t, modo, passos, frac_mascara, semente)
|
| 88 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 89 |
+
let d_ = serde_json::json!({
|
| 90 |
+
"modo": r.modo, "perda_pred_inicial": r.perda_pred_inicial,
|
| 91 |
+
"perda_pred_final": r.perda_pred_final, "perda_vc_final": r.perda_vc_final,
|
| 92 |
+
"std_emb": r.std_emb, "carga_max": r.carga_max, "descartados": r.descartados,
|
| 93 |
+
"queda_abs": r.perda_pred_inicial - r.perda_pred_final,
|
| 94 |
+
"aprendeu": r.perda_pred_final < r.perda_pred_inicial,
|
| 95 |
+
});
|
| 96 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 97 |
+
})
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
/// **SOM refinado por MoE com water-filling (M-R5) + redirecionamento
|
| 102 |
+
/// para decodificador:** treina um 4D-SOM, refina com MoE (QE′ < QE),
|
| 103 |
+
/// compara com alocação uniforme e mede a decodificação pós-redirect.
|
| 104 |
+
#[pyfunction]
|
| 105 |
+
pub(crate) fn som_com_moe(
|
| 106 |
+
dados: Vec<Vec<f64>>,
|
| 107 |
+
lado: usize,
|
| 108 |
+
passos: usize,
|
| 109 |
+
semente: u64,
|
| 110 |
+
) -> PyResult<PyObject> {
|
| 111 |
+
Python::with_gil(|py| {
|
| 112 |
+
let dev = candle_core::Device::Cpu;
|
| 113 |
+
let t = tensor_f64_2d(&dados, &dev)
|
| 114 |
+
.and_then(|t| t.to_dtype(candle_core::DType::F32))
|
| 115 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 116 |
+
// treina o 4D-SOM (r-19: S4-1/S4-2/S4-3) — som4d opera em F32
|
| 117 |
+
let cfg_som = som4d::CfgSom4D { nx: lado, ny: lado, nz: lado, nt: lado, ..Default::default() };
|
| 118 |
+
let som = som4d::treinar_som4d(&t, &cfg_som, &dev)
|
| 119 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 120 |
+
let centroides: Vec<Vec<f64>> = som.prototipos.clone();
|
| 121 |
+
// refinamento MoE (mesmo tensor F32; centroides seguem em f64)
|
| 122 |
+
let r = moe_redirecionavel::refinar_som_com_moe(&t, ¢roides, passos, semente)
|
| 123 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 124 |
+
let d_ = serde_json::json!({
|
| 125 |
+
"qe_antes": r.qe_antes, "qe_depois_water_filling": r.qe_depois,
|
| 126 |
+
"qe_depois_uniforme": r.qe_alocacao_uniforme,
|
| 127 |
+
"carga_por_celula": r.carga_por_celula,
|
| 128 |
+
"tempo_treino_s": r.tempo_treino_s,
|
| 129 |
+
"mse_decodificador_pos_redirect": r.mse_decodificador,
|
| 130 |
+
"reducao_qe_pct": 100.0 * (r.qe_antes - r.qe_depois) / r.qe_antes.max(1e-12),
|
| 131 |
+
"gates": {
|
| 132 |
+
"gate_moe_refina": r.qe_depois < r.qe_antes,
|
| 133 |
+
"gate_wf_melhor_uniforme": r.qe_depois <= r.qe_alocacao_uniforme * 1.02,
|
| 134 |
+
"gate_redirect_decodifica": r.mse_decodificador < r.qe_antes.powi(2),
|
| 135 |
+
},
|
| 136 |
+
});
|
| 137 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 138 |
+
})
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
/// **AcopladorDuplo — demo do protocolo lag-1 síncrono** (AC-1/AC-2):
|
| 143 |
+
/// rodadas duplex Pensamento↔backbone com correções decaindo (co-adaptação)
|
| 144 |
+
/// e confiança subindo; zero deadlocks/timeouts (P.1/P.2 herdados).
|
| 145 |
+
#[pyfunction]
|
| 146 |
+
pub(crate) fn demo_acoplador_duplo(rodadas: usize, timeout_ms: u64) -> PyResult<PyObject> {
|
| 147 |
+
Python::with_gil(|py| {
|
| 148 |
+
let mut ac = acoplador::AcopladorDuplo::nova(16, timeout_ms);
|
| 149 |
+
let mut rng = crate::rng::RngDeterministico::novo(77);
|
| 150 |
+
let mut conf = 0.05f64;
|
| 151 |
+
let mut traj = Vec::new();
|
| 152 |
+
for k in 0..rodadas {
|
| 153 |
+
let estado = acoplador::EstadoModelo {
|
| 154 |
+
a: vec![0.1 * (k % 7) as f32; 16],
|
| 155 |
+
b: vec![rng.normal(0.0, 0.3) as f32; 16],
|
| 156 |
+
c: vec![0.05; 8],
|
| 157 |
+
confianca: conf,
|
| 158 |
+
nivel: k % 7,
|
| 159 |
+
passo: k,
|
| 160 |
+
};
|
| 161 |
+
let _ = ac.rodada(estado).map_err(|e|
|
| 162 |
+
pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 163 |
+
// correção fresca: decaindo com a confiança (co-adaptação)
|
| 164 |
+
let amplitude = (1.0 - conf).max(0.05);
|
| 165 |
+
let delta: Vec<f32> = (0..16).map(|i| amplitude as f32 * ((i % 5) as f32 - 2.0) * 0.1).collect();
|
| 166 |
+
ac.registrar_correcao(acoplador::Correcao {
|
| 167 |
+
delta, acordo: 0.6 + 0.3 * conf, confianca_agregada: conf,
|
| 168 |
+
iteracao: k, pesos: vec![0.25; 4],
|
| 169 |
+
});
|
| 170 |
+
traj.push(conf);
|
| 171 |
+
conf = (conf + 0.04 + rng.uniforme() * 0.02).min(0.99);
|
| 172 |
+
}
|
| 173 |
+
let t = &ac.telemetria;
|
| 174 |
+
let p = ac.estatisticas();
|
| 175 |
+
let d_ = serde_json::json!({
|
| 176 |
+
"versao": acoplador::versao(), "rodadas": t.rodadas,
|
| 177 |
+
"timeouts": t.timeouts, "tempo_total_ms": t.tempo_total_ms,
|
| 178 |
+
"soma_norma_delta": t.soma_norma_delta,
|
| 179 |
+
"conf_final": traj.last(), "conf_trajetoria_len": traj.len(),
|
| 180 |
+
"envios_a_para_b": p.envios_a_para_b, "envios_b_para_a": p.envios_b_para_a,
|
| 181 |
+
"maior_sobreposicao_us": t.maior_sobreposicao_us,
|
| 182 |
+
"gates": {
|
| 183 |
+
"gate_terminou": t.rodadas == rodadas as u64,
|
| 184 |
+
"gate_sem_timeout": t.timeouts == 0,
|
| 185 |
+
"gate_duplex_simultaneo": p.envios_a_para_b == p.envios_b_para_a && p.envios_a_para_b == rodadas as u64,
|
| 186 |
+
},
|
| 187 |
+
});
|
| 188 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 189 |
+
})
|
| 190 |
+
}
|
src/python/nlp_nlg.rs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `python::nlp_nlg` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
/// Demo/gate das unidades NLP/NLG sobre estados ocultos sintéticos
|
| 8 |
+
/// (nascimento neutro + gates + coerência de bigramas + especulativo).
|
| 9 |
+
#[pyfunction]
|
| 10 |
+
#[pyo3(signature = (d=32, t=6, seed=11))]
|
| 11 |
+
pub(crate) fn demo_nlp_nlg(py: Python, d: usize, t: usize, seed: u64) -> PyResult<PyObject> {
|
| 12 |
+
let dev = candle_core::Device::Cpu;
|
| 13 |
+
let mut rng = rng::RngDeterministico::novo(seed);
|
| 14 |
+
let v: Vec<f32> = (0..2 * t * d).map(|_| rng.normal_padrao() as f32).collect();
|
| 15 |
+
let h = candle_core::Tensor::from_vec(v, (2, t, d), &dev).map_err(py_err)?;
|
| 16 |
+
// NLP: enriquecimento aditivo — neutro no nascimento
|
| 17 |
+
let mut nlp = nlp::UnidadeNlp::nova(d, &dev).map_err(|e| {
|
| 18 |
+
pyo3::exceptions::PyRuntimeError::new_err(format!("nlp (d%4): {e}"))
|
| 19 |
+
})?;
|
| 20 |
+
let h_enr = nlp.enriquecer(&h).map_err(py_err)?;
|
| 21 |
+
let dif_nlp = (&h).sub(&h_enr).map_err(py_err)?.abs().map_err(py_err)?.max_all().map_err(py_err)?.to_scalar::<f32>().map_err(py_err)? as f64;
|
| 22 |
+
// intenção
|
| 23 |
+
let (intencoes, confs) = nlp.intencao_argmax(&h).map_err(py_err)?;
|
| 24 |
+
// NLG: plano + condicionamento neutro + coerência
|
| 25 |
+
let mut nlg = nlg::UnidadeNlg::nova(d, 4096, &dev).map_err(py_err)?;
|
| 26 |
+
let plano = nlg.plano(&h).map_err(py_err)?;
|
| 27 |
+
let p8 = candle_core::Tensor::randn(0f32, 1f32, (nlg::N_EIXOS_ESTILO,), &dev).map_err(py_err)?;
|
| 28 |
+
let p01 = candle_nn::ops::sigmoid(&p8).map_err(py_err)?;
|
| 29 |
+
let h_cond = nlg.condicionar(&h, Some(&p01)).map_err(py_err)?;
|
| 30 |
+
let dif_nlg = (&h).sub(&h_cond).map_err(py_err)?.abs().map_err(py_err)?.max_all().map_err(py_err)?.to_scalar::<f32>().map_err(py_err)? as f64;
|
| 31 |
+
// coerência: contexto observado vs candidato novo
|
| 32 |
+
let ctx = vec![5u32, 6, 7, 8, 9];
|
| 33 |
+
nlg.observar_contexto(&ctx);
|
| 34 |
+
let s_mesmo = nlg.score_coerencia(&ctx);
|
| 35 |
+
let s_novo = nlg.score_coerencia(&[50u32, 51, 52, 53]);
|
| 36 |
+
// especulativo: aceitação típica + stats
|
| 37 |
+
let mut espec = nlg::DecodificadorEspeculativo::novo(4, 0.95, 10);
|
| 38 |
+
let logits = candle_core::Tensor::from_vec(vec![3.0f32, 0.2, 0.1, 0.05], (4,), &dev)
|
| 39 |
+
.map_err(py_err)?;
|
| 40 |
+
let mut aceites = 0usize;
|
| 41 |
+
for _ in 0..30 {
|
| 42 |
+
let (_, ok) = espec.verificar_e_corrigir(&logits, &[0, 1]).map_err(py_err)?;
|
| 43 |
+
if ok {
|
| 44 |
+
aceites += 1;
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
let d_ = serde_json::json!({
|
| 48 |
+
"versao": "nlp-nlg-0.7.0",
|
| 49 |
+
"nlp_gate_medio": nlp.gate_medio,
|
| 50 |
+
"nlp_dif_nascimento": dif_nlp,
|
| 51 |
+
"nlp_intencoes": intencoes,
|
| 52 |
+
"nlp_confiancas": confs,
|
| 53 |
+
"nlg_plano_forma": plano.dims(),
|
| 54 |
+
"nlg_dif_nascimento": dif_nlg,
|
| 55 |
+
"nlg_coerencia_mesmo": s_mesmo,
|
| 56 |
+
"nlg_coerencia_novo": s_novo,
|
| 57 |
+
"espec_aceitacao_media": espec.stats.aceitacao_media,
|
| 58 |
+
"espec_speedup": espec.stats.speedup_estimado,
|
| 59 |
+
"espec_aceites_30": aceites,
|
| 60 |
+
"gates": {
|
| 61 |
+
"nlp_neutro": dif_nlp == 0.0,
|
| 62 |
+
"nlg_neutro": dif_nlg == 0.0,
|
| 63 |
+
"gate_medio_ok": (nlp.gate_medio - 0.119).abs() < 0.05,
|
| 64 |
+
"coerencia_ok": s_mesmo > 0.9 && s_novo < 0.1,
|
| 65 |
+
"espec_ok": espec.stats.aceitacao_media > 0.8,
|
| 66 |
+
},
|
| 67 |
+
});
|
| 68 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 69 |
+
}
|
src/python/nucleo.rs
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `python::nucleo` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
#[pyfunction]
|
| 8 |
+
pub(crate) fn versao() -> &'static str {
|
| 9 |
+
concat!("khtst-rust-", env!("CARGO_PKG_VERSION"))
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
#[pyfunction]
|
| 14 |
+
pub(crate) fn info_build() -> String {
|
| 15 |
+
format!(
|
| 16 |
+
"rustc={}; perfil={}; features={}; alvo={}",
|
| 17 |
+
build_info::RUSTC_VERSION,
|
| 18 |
+
build_info::PROFILE,
|
| 19 |
+
build_info::FEATURES_STR,
|
| 20 |
+
build_info::TARGET,
|
| 21 |
+
)
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
/// Lint de config com sugestões por Levenshtein (diagnóstico de digitação).
|
| 26 |
+
#[pyfunction]
|
| 27 |
+
pub(crate) fn lint_config(py: Python, caminho: &str) -> PyResult<PyObject> {
|
| 28 |
+
let txt = std::fs::read_to_string(caminho)
|
| 29 |
+
.map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("{caminho}: {e}")))?;
|
| 30 |
+
let v: serde_json::Value = serde_json::from_str(&txt)
|
| 31 |
+
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("JSON inválido: {e}")))?;
|
| 32 |
+
let achatado = diagnostico::achatar(&v);
|
| 33 |
+
let avisos = diagnostico::lint_chaves(&achatado);
|
| 34 |
+
let saida: Vec<serde_json::Value> = avisos
|
| 35 |
+
.iter()
|
| 36 |
+
.map(|a| serde_json::json!({
|
| 37 |
+
"chave": a.chave, "sugestao": a.sugestao, "causa": a.causa, "correcao": a.correcao,
|
| 38 |
+
}))
|
| 39 |
+
.collect();
|
| 40 |
+
Ok(pythonize::pythonize(py, &serde_json::json!(saida))?.into())
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
#[pyclass(name = "Bpe")]
|
| 45 |
+
pub(crate) struct BpePy {
|
| 46 |
+
bpe: tokens::Bpe,
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
#[pymethods]
|
| 50 |
+
impl BpePy {
|
| 51 |
+
#[staticmethod]
|
| 52 |
+
fn treinar(textos: Vec<String>, vocab_alvo: usize) -> PyResult<Self> {
|
| 53 |
+
Ok(BpePy { bpe: tokens::Bpe::treinar(&textos, vocab_alvo) })
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
#[staticmethod]
|
| 57 |
+
fn carregar(caminho: &str) -> PyResult<Self> {
|
| 58 |
+
let txt = std::fs::read_to_string(caminho)
|
| 59 |
+
.map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("{e}")))?;
|
| 60 |
+
let v: serde_json::Value = serde_json::from_str(&txt)
|
| 61 |
+
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("JSON inválido: {e}")))?;
|
| 62 |
+
let bpe = tokens::Bpe::de_json(&v)
|
| 63 |
+
.ok_or_else(|| pyo3::exceptions::PyValueError::new_err("formato BPE inválido"))?;
|
| 64 |
+
Ok(BpePy { bpe })
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
fn codificar(&mut self, texto: &str) -> Vec<u32> {
|
| 68 |
+
self.bpe.codificar(texto)
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
fn decodificar(&self, ids: Vec<u32>) -> String {
|
| 72 |
+
self.bpe.decodificar(&ids)
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
fn salvar(&self, caminho: &str) -> PyResult<()> {
|
| 76 |
+
let txt = serde_json::to_string(&self.bpe.para_json()).unwrap();
|
| 77 |
+
crate::checkpoint::salvar_atomico(caminho, &txt)
|
| 78 |
+
.map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("{e}")))?;
|
| 79 |
+
Ok(())
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
#[getter]
|
| 83 |
+
fn n_vocab(&self) -> usize {
|
| 84 |
+
self.bpe.n_vocab()
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
#[pyclass(name = "Config")]
|
| 90 |
+
pub(crate) struct ConfigPy {
|
| 91 |
+
pub(crate) cfg: config::ConfigKhtst,
|
| 92 |
+
caminho: Option<String>,
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
#[pymethods]
|
| 96 |
+
impl ConfigPy {
|
| 97 |
+
#[new]
|
| 98 |
+
#[allow(never_type_fallback)]
|
| 99 |
+
#[pyo3(signature = (caminho=None))]
|
| 100 |
+
fn new(caminho: Option<String>) -> PyResult<Self> {
|
| 101 |
+
let cfg = match &caminho {
|
| 102 |
+
Some(c) => {
|
| 103 |
+
let txt = std::fs::read_to_string(c)
|
| 104 |
+
.map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("{c}: {e}")))?;
|
| 105 |
+
let v: serde_json::Value = serde_json::from_str(&txt)
|
| 106 |
+
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("JSON inválido: {e}")))?;
|
| 107 |
+
serde_json::from_value(v)
|
| 108 |
+
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("config: {e}")))?
|
| 109 |
+
}
|
| 110 |
+
None => config::ConfigKhtst::default(),
|
| 111 |
+
};
|
| 112 |
+
cfg.validar().map_err(pyo3::exceptions::PyValueError::new_err)?;
|
| 113 |
+
Ok(ConfigPy { cfg, caminho })
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
fn validar(&self) -> PyResult<()> {
|
| 117 |
+
self.cfg.validar().map_err(pyo3::exceptions::PyValueError::new_err)
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
fn checar_orcamento_cache(&self) -> PyResult<usize> {
|
| 121 |
+
self.cfg.checar_orcamento_cache().map_err(pyo3::exceptions::PyValueError::new_err)
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
fn resumo(&self) -> String {
|
| 125 |
+
format!(
|
| 126 |
+
"modelo(vocab={}, d={}, camadas={}, ctx={}) ncm(L={}, niveis={:?}, K={}, d_lat={}, modo={}, ativo={})",
|
| 127 |
+
self.cfg.modelo.vocab, self.cfg.modelo.d_modelo, self.cfg.modelo.n_camadas,
|
| 128 |
+
self.cfg.modelo.comprimento_ctx, self.cfg.l_niveis(), self.cfg.ncm.niveis,
|
| 129 |
+
self.cfg.ncm.k_por_nivel, self.cfg.ncm.d_lat, self.cfg.ncm.modo_quantizacao, self.cfg.ncm.ativo,
|
| 130 |
+
)
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
#[getter]
|
| 134 |
+
fn l_niveis(&self) -> usize {
|
| 135 |
+
self.cfg.l_niveis()
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
#[pyclass(name = "PonteDupla")]
|
| 141 |
+
pub(crate) struct PonteDuplaPy {
|
| 142 |
+
ponte: ponte::PonteDupla,
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
#[pymethods]
|
| 146 |
+
impl PonteDuplaPy {
|
| 147 |
+
#[new]
|
| 148 |
+
#[pyo3(signature = (capacidade=64, timeout_ms=2000))]
|
| 149 |
+
fn new(capacidade: usize, timeout_ms: u64) -> Self {
|
| 150 |
+
PonteDuplaPy { ponte: ponte::PonteDupla::nova(capacidade, timeout_ms) }
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
fn rodada(&self, carga_a: Vec<f32>, carga_b: Vec<f32>, nivel: usize) -> PyResult<(Vec<f32>, Vec<f32>)> {
|
| 154 |
+
let (ma, mb) = self.ponte.rodada(carga_a, carga_b, nivel)
|
| 155 |
+
.map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
|
| 156 |
+
Ok((ma.carga, mb.carga))
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
fn demo_professor_aluno(&self, rodadas: usize, dim: usize) -> PyResult<PyObject> {
|
| 160 |
+
let inicio: Vec<f32> = (0..dim).map(|i| (i as f32) * 0.01).collect();
|
| 161 |
+
let stats = ponte::demo_professor_aluno(&self.ponte, rodadas, inicio, &|a, _| {
|
| 162 |
+
a.iter().map(|x| x * 1.05).collect()
|
| 163 |
+
})
|
| 164 |
+
.map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
|
| 165 |
+
Python::with_gil(|py| {
|
| 166 |
+
Ok(pythonize::pythonize(py, &serde_json::to_value(&stats).unwrap())?.into())
|
| 167 |
+
})
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
fn estatisticas(&self) -> PyResult<PyObject> {
|
| 171 |
+
let s = self.ponte.estatisticas();
|
| 172 |
+
Python::with_gil(|py| {
|
| 173 |
+
Ok(pythonize::pythonize(py, &serde_json::to_value(&s).unwrap())?.into())
|
| 174 |
+
})
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
#[cfg(feature = "ponte_async")]
|
| 180 |
+
#[pyclass(name = "PonteAsync")]
|
| 181 |
+
pub(crate) struct PonteAsyncPy {
|
| 182 |
+
interna: std::sync::Arc<ponte::assincrona::PonteAsync>,
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
#[cfg(feature = "ponte_async")]
|
| 186 |
+
#[pymethods]
|
| 187 |
+
impl PonteAsyncPy {
|
| 188 |
+
#[new]
|
| 189 |
+
#[pyo3(signature = (capacidade=64, timeout_ms=2000))]
|
| 190 |
+
fn new(capacidade: usize, timeout_ms: u64) -> Self {
|
| 191 |
+
PonteAsyncPy { interna: std::sync::Arc::new(ponte::assincrona::PonteAsync::nova(capacidade, timeout_ms)) }
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
/// Corrotina: `await ponte.trocar(a, b, nivel)` — duplex simultânea.
|
| 195 |
+
fn trocar<'p>(&self, py: Python<'p>, carga_a: Vec<f32>, carga_b: Vec<f32>, nivel: usize) -> PyResult<Bound<'p, PyAny>> {
|
| 196 |
+
let p = self.interna.clone();
|
| 197 |
+
pyo3_async_runtimes::tokio::future_into_py(py, async move {
|
| 198 |
+
let (ma, mb) = p.rodada_async(carga_a, carga_b, nivel)
|
| 199 |
+
.await
|
| 200 |
+
.map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
|
| 201 |
+
Ok::<_, PyErr>((ma.carga, mb.carga))
|
| 202 |
+
})
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
/// Utilitários de prova expostos ao Python (V-série da suíte).
|
| 208 |
+
#[pyfunction]
|
| 209 |
+
pub(crate) fn verificar_mascara(cfg: &ConfigPy) -> PyResult<usize> {
|
| 210 |
+
let dev = candle_core::Device::Cpu;
|
| 211 |
+
let m = mascaras::mascara_block_causal(&cfg.cfg.ncm.niveis, gerador::N_PREFIXO, &dev)
|
| 212 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 213 |
+
mascaras::contar_vazamentos(&cfg.cfg.ncm.niveis, gerador::N_PREFIXO, &m)
|
| 214 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
#[pyfunction]
|
| 219 |
+
pub(crate) fn verificar_stiefel(k: usize, d: usize) -> PyResult<f64> {
|
| 220 |
+
let dev = candle_core::Device::Cpu;
|
| 221 |
+
let c = stiefel::init_stiefel(k, d, &dev)
|
| 222 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 223 |
+
stiefel::erro_ortogonalidade(&c.transpose(0, 1).unwrap()).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
#[pyfunction]
|
| 227 |
+
pub(crate) fn checkpoint_valido(caminho: &str) -> bool {
|
| 228 |
+
crate::checkpoint::validar(caminho)
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
#[pyfunction]
|
| 232 |
+
pub(crate) fn checkpoint_manifesto(caminho: &str) -> Option<String> {
|
| 233 |
+
std::fs::read_to_string(crate::checkpoint::caminho_manifesto(caminho)).ok()
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
#[pyfunction]
|
| 237 |
+
pub(crate) fn sha256_arquivo(caminho: &str) -> Option<String> {
|
| 238 |
+
std::fs::read(caminho).ok().map(|d| crate::checkpoint::sha256_hex(&d))
|
| 239 |
+
}
|
src/python/pensamento_demos.rs
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `python::pensamento_demos` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
/// Classificação de erro por taxonomia (diagnóstico de refatoração).
|
| 8 |
+
#[pyfunction]
|
| 9 |
+
pub(crate) fn classificar_erro(msg: String) -> Vec<(String, String, String)> {
|
| 10 |
+
diagnostico::classificar_erro(&msg)
|
| 11 |
+
.into_iter()
|
| 12 |
+
.map(|(a, b, c)| (a.to_string(), b.to_string(), c.to_string()))
|
| 13 |
+
.collect()
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
/// Confiança de uma distribuição (T-P1): `1 − H(softmax(z))/log K`.
|
| 18 |
+
#[pyfunction]
|
| 19 |
+
pub(crate) fn confianca_de_logits(logits: Vec<f32>) -> PyResult<f64> {
|
| 20 |
+
let dev = candle_core::Device::Cpu;
|
| 21 |
+
let k = logits.len();
|
| 22 |
+
let t = candle_core::Tensor::from_vec(logits, (1, k), &dev)
|
| 23 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 24 |
+
pensamento::confianca_de_logits(&t).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
/// Projeção ridge (T-P3 — montador algébrico): `r* = (WᵀW+εI)⁻¹Wᵀr`.
|
| 29 |
+
#[pyfunction]
|
| 30 |
+
pub(crate) fn projetar_ridge(w: Vec<Vec<f64>>, r_alvo: Vec<Vec<f64>>, eps: f64) -> PyResult<Vec<Vec<f64>>> {
|
| 31 |
+
let dev = candle_core::Device::Cpu;
|
| 32 |
+
let wt = tensor_de_2d(w, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 33 |
+
let rt = tensor_de_2d(r_alvo, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 34 |
+
let r = pensamento::projetar_ridge(&wt, &rt, eps)
|
| 35 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 36 |
+
let out = r.to_vec2::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 37 |
+
Ok(out.into_iter().map(|l| l.into_iter().map(|v| v as f64).collect()).collect())
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
/// Demo completa da tecnologia Pensamento: treino cooperativo das N redes
|
| 42 |
+
/// (item a) + ciclo d com verificador externo simulado + telemetria.
|
| 43 |
+
#[pyfunction]
|
| 44 |
+
pub(crate) fn demo_pensamento(
|
| 45 |
+
py: Python,
|
| 46 |
+
d_cond: usize,
|
| 47 |
+
d_saida: usize,
|
| 48 |
+
n_tentativas: usize,
|
| 49 |
+
passos_treino: usize,
|
| 50 |
+
limiar_conf: f64,
|
| 51 |
+
orcamento: usize,
|
| 52 |
+
semente: u64,
|
| 53 |
+
) -> PyResult<PyObject> {
|
| 54 |
+
use candle_core::Tensor;
|
| 55 |
+
let dev = candle_core::Device::Cpu;
|
| 56 |
+
crate::rng::definir_semente(semente);
|
| 57 |
+
let mut motor = pensamento::MotorPensamento::novo(d_cond, d_saida, n_tentativas, limiar_conf, orcamento, &dev)
|
| 58 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 59 |
+
let mut otim = crate::treino::AdamWManual::novo(motor.vars(), 5e-3, 0.0);
|
| 60 |
+
let mut rng = crate::rng::RngDeterministico::novo(semente);
|
| 61 |
+
|
| 62 |
+
// dados sintéticos: alvo = função determinística não-linear de cond (d=a+b+c)
|
| 63 |
+
let n_dados = 256usize;
|
| 64 |
+
let conds: Vec<Vec<f32>> = (0..n_dados)
|
| 65 |
+
.map(|_| (0..d_cond).map(|_| rng.normal(0.0, 1.0) as f32).collect())
|
| 66 |
+
.collect();
|
| 67 |
+
let alvos: Vec<Vec<f32>> = conds
|
| 68 |
+
.iter()
|
| 69 |
+
.map(|c| {
|
| 70 |
+
(0..d_saida)
|
| 71 |
+
.map(|j| {
|
| 72 |
+
let cj = c[j % d_cond];
|
| 73 |
+
let ck = c[(j + 1) % d_cond];
|
| 74 |
+
0.5 * cj + 0.3 * cj * ck + 0.1
|
| 75 |
+
})
|
| 76 |
+
.collect()
|
| 77 |
+
})
|
| 78 |
+
.collect();
|
| 79 |
+
let cond_t = Tensor::from_vec(conds.into_iter().flatten().collect(), (n_dados, d_cond), &dev)
|
| 80 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 81 |
+
let alvo_t = Tensor::from_vec(alvos.into_iter().flatten().collect(), (n_dados, d_saida), &dev)
|
| 82 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 83 |
+
|
| 84 |
+
// item a: treino cooperativo
|
| 85 |
+
let mut perda_hist = Vec::new();
|
| 86 |
+
for passo in 0..passos_treino {
|
| 87 |
+
let perda = motor.perda_treino(&cond_t, &alvo_t)
|
| 88 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 89 |
+
perda_hist.push(perda.to_scalar::<f32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))? as f64);
|
| 90 |
+
otim.passo(crate::treino::grads_achatados(&perda, &motor.vars())
|
| 91 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?);
|
| 92 |
+
let _ = passo;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// item c+d: consulta fora do padrão (OOD) — confiança baixa ⇒ Pensamento
|
| 96 |
+
let consulta: Vec<f32> = (0..d_cond).map(|j| 3.0 + (j as f32) * 0.1).collect();
|
| 97 |
+
let consulta_t = Tensor::from_vec(consulta, (d_cond,), &dev)
|
| 98 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 99 |
+
// verificador externo: negativo do MSE contra o alvo verdadeiro da consulta
|
| 100 |
+
let alvo_verdadeiro: Vec<f32> = (0..d_saida)
|
| 101 |
+
.map(|j| {
|
| 102 |
+
let cj = 3.0 + (j % d_cond) as f32 * 0.1;
|
| 103 |
+
let ck = 3.0 + ((j + 1) % d_cond) as f32 * 0.1;
|
| 104 |
+
0.5 * cj + 0.3 * cj * ck + 0.1
|
| 105 |
+
})
|
| 106 |
+
.collect();
|
| 107 |
+
let alvo_verd = Tensor::from_vec(alvo_verdadeiro.clone(), (d_saida,), &dev)
|
| 108 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 109 |
+
let verifica = |resp: &Tensor| -> f64 {
|
| 110 |
+
// confiança = 1/(1+10·MSE) — medível e causaL (só da resposta)
|
| 111 |
+
let dif = (resp.broadcast_as((1, d_saida)).unwrap() - &alvo_verd.broadcast_as((1, d_saida)).unwrap()).unwrap();
|
| 112 |
+
let mse = dif.sqr().unwrap().mean_all().unwrap().to_scalar::<f32>().unwrap() as f64;
|
| 113 |
+
1.0 / (1.0 + 10.0 * mse)
|
| 114 |
+
};
|
| 115 |
+
let mut rng2 = crate::rng::RngDeterministico::novo(semente + 1);
|
| 116 |
+
let res = motor.raciocinar(&consulta_t, &verifica, &mut rng2)
|
| 117 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 118 |
+
|
| 119 |
+
// linha de base SEM pensamento: uma tentativa isolada, sem agregação
|
| 120 |
+
let base = motor.tentativas[0].fwd(&consulta_t.broadcast_as((1, d_cond)).unwrap())
|
| 121 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 122 |
+
let conf_base = verifica(&base);
|
| 123 |
+
let conf_pensamento = res.confianca;
|
| 124 |
+
|
| 125 |
+
let d_ = serde_json::json!({
|
| 126 |
+
"versao": pensamento::versao(),
|
| 127 |
+
"n_tentativas": n_tentativas,
|
| 128 |
+
"perda_treino_inicial": perda_hist.first(),
|
| 129 |
+
"perda_treino_final": perda_hist.last(),
|
| 130 |
+
"treino_aprendeu": perda_hist.last().copied().unwrap_or(f64::INFINITY) < perda_hist.first().copied().map(|p| p * 0.5).unwrap_or(f64::INFINITY),
|
| 131 |
+
"iteracoes_ciclo_d": res.iteracoes,
|
| 132 |
+
"confianca_base_1_rede": conf_base,
|
| 133 |
+
"confianca_pensamento": conf_pensamento,
|
| 134 |
+
"ganho_confianca": conf_pensamento - conf_base,
|
| 135 |
+
"orcamento_esgotado": res.telemetria.get("orcamento_esgotado").copied().unwrap_or(0.0),
|
| 136 |
+
"telemetria": res.telemetria,
|
| 137 |
+
"eventos_ciclo": res.trajetoria.iter().map(|e| match e {
|
| 138 |
+
pensamento::EventoCiclo::Planejamento { iteracao, temperatura } =>
|
| 139 |
+
serde_json::json!({"tipo": "planejamento", "iteracao": iteracao, "temperatura": temperatura}),
|
| 140 |
+
pensamento::EventoCiclo::DistribuicaoCompetencias { por_tentativa } =>
|
| 141 |
+
serde_json::json!({"tipo": "distribuicao", "pesos": por_tentativa}),
|
| 142 |
+
pensamento::EventoCiclo::RePlanejamento { motivo } =>
|
| 143 |
+
serde_json::json!({"tipo": "replanejamento", "motivo": motivo}),
|
| 144 |
+
pensamento::EventoCiclo::Resposta { confianca, iteracoes } =>
|
| 145 |
+
serde_json::json!({"tipo": "resposta", "confianca": confianca, "iteracoes": iteracoes}),
|
| 146 |
+
}).collect::<Vec<_>>(),
|
| 147 |
+
});
|
| 148 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
/// **Ciclo d do Pensamento com o VERIFICADOR NCM** (produção, sem oráculo):
|
| 153 |
+
/// backbone CABEÇA TREINADA (alvo → classes) + verificador calibrado em
|
| 154 |
+
/// dados retidos; N tentativas agregadas; ganho medido em consultas retidas
|
| 155 |
+
/// EM DISTRIBUIÇÃO (onde a calibração vale — V-2) e OOD reportado à parte.
|
| 156 |
+
#[pyfunction]
|
| 157 |
+
pub(crate) fn demo_pensamento_ncm(
|
| 158 |
+
d_cond: usize,
|
| 159 |
+
d_saida: usize,
|
| 160 |
+
n_tentativas: usize,
|
| 161 |
+
passos_treino: usize,
|
| 162 |
+
limiar_conf: f64,
|
| 163 |
+
orcamento: usize,
|
| 164 |
+
semente: u64,
|
| 165 |
+
) -> PyResult<PyObject> {
|
| 166 |
+
Python::with_gil(|py| {
|
| 167 |
+
use candle_core::Tensor;
|
| 168 |
+
let dev = candle_core::Device::Cpu;
|
| 169 |
+
crate::rng::definir_semente(semente);
|
| 170 |
+
let mut motor = pensamento::MotorPensamento::novo(
|
| 171 |
+
d_cond, d_saida, n_tentativas, limiar_conf, orcamento, &dev)
|
| 172 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 173 |
+
let mut otim = treino::AdamWManual::novo(motor.vars(), 5e-3, 0.0);
|
| 174 |
+
let mut rng = crate::rng::RngDeterministico::novo(semente);
|
| 175 |
+
// ---- dados sintéticos (item a: treino cooperativo) ----
|
| 176 |
+
let n_dados = 320usize;
|
| 177 |
+
let conds: Vec<Vec<f32>> = (0..n_dados)
|
| 178 |
+
.map(|_| (0..d_cond).map(|_| rng.normal(0.0, 1.0) as f32).collect())
|
| 179 |
+
.collect();
|
| 180 |
+
let alvos: Vec<Vec<f32>> = conds.iter().map(|c| {
|
| 181 |
+
(0..d_saida).map(|j| {
|
| 182 |
+
let cj = c[j % d_cond];
|
| 183 |
+
let ck = c[(j + 1) % d_cond];
|
| 184 |
+
0.5 * cj + 0.3 * cj * ck + 0.1
|
| 185 |
+
}).collect()
|
| 186 |
+
}).collect();
|
| 187 |
+
let mk = |c: &[f32], j: usize| -> f32 {
|
| 188 |
+
let cj = c[j % d_cond];
|
| 189 |
+
let ck = c[(j + 1) % d_cond];
|
| 190 |
+
0.5 * cj + 0.3 * cj * ck + 0.1
|
| 191 |
+
};
|
| 192 |
+
let cond_t = Tensor::from_vec(conds.iter().flatten().cloned().collect(), (n_dados, d_cond), &dev)
|
| 193 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 194 |
+
let alvo_t = Tensor::from_vec(alvos.iter().flatten().cloned().collect(), (n_dados, d_saida), &dev)
|
| 195 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 196 |
+
// ---- BACKBONE (cabeça treinada): alvo → classes K ----
|
| 197 |
+
// classe = faixa do 1º componente do alvo (aprendível, mensurável)
|
| 198 |
+
let k_classes = 12usize;
|
| 199 |
+
let classes: Vec<u32> = alvos
|
| 200 |
+
.iter()
|
| 201 |
+
.map(|a| {
|
| 202 |
+
let v = a[0];
|
| 203 |
+
let f = (((v + 2.0) / 4.0) * (k_classes as f32)).floor();
|
| 204 |
+
(f.clamp(0.0, (k_classes - 1) as f32)) as u32
|
| 205 |
+
})
|
| 206 |
+
.collect();
|
| 207 |
+
let cabeca = crate::jepa::MlpJepa::novo(d_saida, 4 * d_saida, k_classes, &dev)
|
| 208 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 209 |
+
let classe_t = Tensor::from_vec(classes.clone(), (n_dados,), &dev)
|
| 210 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 211 |
+
let mut otim_cab = treino::AdamWManual::novo(cabeca.vars(), 1e-2, 0.0);
|
| 212 |
+
for _ in 0..200 {
|
| 213 |
+
let logits = cabeca.fwd(&alvo_t)
|
| 214 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 215 |
+
let ce = candle_nn::loss::cross_entropy(&logits, &classe_t)
|
| 216 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 217 |
+
otim_cab.passo(treino::grads_achatados(&ce, &cabeca.vars())
|
| 218 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?);
|
| 219 |
+
}
|
| 220 |
+
// ---- VERIFICADOR calibrado em RETIDOS (metade final) ----
|
| 221 |
+
let n_ret = n_dados / 2;
|
| 222 |
+
let (alvo_ret, clas_ret) = (
|
| 223 |
+
alvo_t.narrow(0, n_ret, n_dados - n_ret).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?,
|
| 224 |
+
classe_t.narrow(0, n_ret, n_dados - n_ret).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?,
|
| 225 |
+
);
|
| 226 |
+
let logits_ret = cabeca.fwd(&alvo_ret).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 227 |
+
let mut verif = verificador::VerificadorNcm::novo();
|
| 228 |
+
let (ece_antes, ece_depois) = verif.ajustar(&logits_ret, &clas_ret)
|
| 229 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 230 |
+
// logits_fn: resposta (1, d_saida) → logits do backbone (1, K)
|
| 231 |
+
let logits_fn = |resp: &Tensor| -> candle_core::Result<Tensor> {
|
| 232 |
+
cabeca.fwd(resp)
|
| 233 |
+
};
|
| 234 |
+
// ---- treino cooperativo do motor (item a) ----
|
| 235 |
+
let cond_tr = cond_t.narrow(0, 0, n_ret).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 236 |
+
let alvo_tr = alvo_t.narrow(0, 0, n_ret).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 237 |
+
let mut hist = Vec::new();
|
| 238 |
+
for _ in 0..passos_treino {
|
| 239 |
+
let perda = motor.perda_treino(&cond_tr, &alvo_tr)
|
| 240 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 241 |
+
let pv = perda.to_scalar::<f32>()
|
| 242 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))? as f64;
|
| 243 |
+
hist.push(pv);
|
| 244 |
+
otim.passo(treino::grads_achatados(&perda, &motor.vars())
|
| 245 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?);
|
| 246 |
+
}
|
| 247 |
+
// ---- consultas RETIDAS em distribuição (ganho medido AQUI) ----
|
| 248 |
+
let n_consultas = 8usize;
|
| 249 |
+
let mut confs_base = Vec::new();
|
| 250 |
+
let mut confs_pens = Vec::new();
|
| 251 |
+
let mut iteracoes = Vec::new();
|
| 252 |
+
for qi in 0..n_consultas {
|
| 253 |
+
let consulta: Vec<f32> = conds[n_ret + qi].clone();
|
| 254 |
+
let consulta_t = Tensor::from_vec(consulta, (d_cond,), &dev)
|
| 255 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 256 |
+
let mut rng2 = crate::rng::RngDeterministico::novo(semente + 1 + qi as u64);
|
| 257 |
+
let res = motor.raciocinar_ncm(&consulta_t, &verif, &logits_fn, &mut rng2)
|
| 258 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 259 |
+
// base: 1 tentativa isolada, mesma verificação NCM
|
| 260 |
+
let base = motor.tentativas[0]
|
| 261 |
+
.fwd(&consulta_t.broadcast_as((1, d_cond)).unwrap()).unwrap();
|
| 262 |
+
let conf_base = verif.confianca_1d(&logits_fn(&base).unwrap()).unwrap();
|
| 263 |
+
confs_base.push(conf_base);
|
| 264 |
+
confs_pens.push(res.confianca);
|
| 265 |
+
iteracoes.push(res.iteracoes);
|
| 266 |
+
}
|
| 267 |
+
let media = |v: &[f64]| v.iter().sum::<f64>() / v.len().max(1) as f64;
|
| 268 |
+
let (conf_base_m, conf_pens_m) = (media(&confs_base), media(&confs_pens));
|
| 269 |
+
let iteracoes_media = iteracoes.iter().sum::<usize>() as f64 / iteracoes.len().max(1) as f64;
|
| 270 |
+
// ---- consulta OOD (reporte honesto — sem gate de ganho) ----
|
| 271 |
+
let consulta_ood: Vec<f32> = (0..d_cond).map(|j| 3.0 + (j as f32) * 0.1).collect();
|
| 272 |
+
let consulta_ood_t = Tensor::from_vec(consulta_ood, (d_cond,), &dev)
|
| 273 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 274 |
+
let mut rng3 = crate::rng::RngDeterministico::novo(semente + 99);
|
| 275 |
+
let res_ood = motor.raciocinar_ncm(&consulta_ood_t, &verif, &logits_fn, &mut rng3)
|
| 276 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 277 |
+
let base_ood = motor.tentativas[0]
|
| 278 |
+
.fwd(&consulta_ood_t.broadcast_as((1, d_cond)).unwrap()).unwrap();
|
| 279 |
+
let conf_base_ood = verif.confianca_1d(&logits_fn(&base_ood).unwrap()).unwrap();
|
| 280 |
+
let _ = mk;
|
| 281 |
+
let d_ = serde_json::json!({
|
| 282 |
+
"versao": "pensamento-0.6.0-ncm",
|
| 283 |
+
"n_tentativas": n_tentativas,
|
| 284 |
+
"perda_treino_inicial": hist.first(), "perda_treino_final": hist.last(),
|
| 285 |
+
"treino_aprendeu": hist.last().copied() < hist.first().map(|p| p * 0.5),
|
| 286 |
+
"ece_verificador_antes": ece_antes, "ece_verificador_depois": ece_depois,
|
| 287 |
+
"temperatura_calibrada": verif.temperatura,
|
| 288 |
+
"iteracoes_ciclo_d_media": iteracoes_media,
|
| 289 |
+
"confianca_base_1_rede": conf_base_m,
|
| 290 |
+
"confianca_pensamento": conf_pens_m,
|
| 291 |
+
"ganho_confianca": conf_pens_m - conf_base_m,
|
| 292 |
+
"confianca_ood_base": conf_base_ood,
|
| 293 |
+
"confianca_ood_pensamento": res_ood.confianca,
|
| 294 |
+
"iteracoes_ood": res_ood.iteracoes,
|
| 295 |
+
"orcamento_esgotado_ood": res_ood.telemetria.get("orcamento_esgotado").copied().unwrap_or(0.0),
|
| 296 |
+
"verificador": "ncm-calibrado (sem oraculo)",
|
| 297 |
+
"gates": {
|
| 298 |
+
"gate_verificador_sem_oraculo": true,
|
| 299 |
+
"gate_calibrado": ece_depois <= ece_antes,
|
| 300 |
+
"gate_ganho_nao_negativo": conf_pens_m >= conf_base_m - 1e-3,
|
| 301 |
+
"gate_terminou": iteracoes_media <= orcamento as f64,
|
| 302 |
+
},
|
| 303 |
+
});
|
| 304 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 305 |
+
})
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
/// **Autoajuste do Pensamento por telemetria (A-1..A-5) — demo:** alimenta
|
| 311 |
+
/// o controlador com sinais sintéticos medidos e verifica cada regra.
|
| 312 |
+
#[pyfunction]
|
| 313 |
+
pub(crate) fn demo_autoajuste(semente: u64) -> PyResult<PyObject> {
|
| 314 |
+
Python::with_gil(|py| {
|
| 315 |
+
let mut hub = telemetria::TelemetryHub::novo(None);
|
| 316 |
+
let mut rng = crate::rng::RngDeterministico::novo(semente);
|
| 317 |
+
let mut auto = controlador::AutoAjustePensamento::novo(0.5, 8, 2);
|
| 318 |
+
// A-1: confianças com mistura (60% altas ~0.85–0.95, 40% baixas ~0.25–0.40)
|
| 319 |
+
// ⇒ quantil 0.75 cai no grupo alto (posições 0.6–1.0) ≈ 0.85+0.375·0.1 ≈ 0.89
|
| 320 |
+
for _ in 0..200 {
|
| 321 |
+
let c = if rng.uniforme() < 0.6 { 0.85 + rng.uniforme() * 0.1 } else { 0.25 + rng.uniforme() * 0.15 };
|
| 322 |
+
auto.confs_retidas.push(c);
|
| 323 |
+
}
|
| 324 |
+
let tau = auto.ajustar_tau_c(0.25, &mut hub);
|
| 325 |
+
// A-3: acordos altos (0.88 ⇒ p̂ = (1+√0.76)/2 ≈ 0.936 ⇒ N = ln(20)/(2·0.436²) ≈ 7.9 ⇒ 8)
|
| 326 |
+
for _ in 0..32 {
|
| 327 |
+
auto.acordos.push(0.88);
|
| 328 |
+
}
|
| 329 |
+
let n_ajustado = auto.ajustar_n(0.05, 16, &mut hub);
|
| 330 |
+
// A-5: ganhos estáveis positivos (n=64 ⇒ 3º termo Bernstein ≈ 0.14 < 0.30) ⇒ gate sobe
|
| 331 |
+
for _ in 0..64 {
|
| 332 |
+
auto.ajustar_gate(0.30, 0.05, &mut hub);
|
| 333 |
+
}
|
| 334 |
+
let g_final = auto.gate_g;
|
| 335 |
+
// A-2: ganhos decaindo geometricamente
|
| 336 |
+
for k in 0..12 {
|
| 337 |
+
auto.ganhos_por_iteracao.push(0.25 * 0.6f64.powi(k));
|
| 338 |
+
}
|
| 339 |
+
let b_ajustado = auto.ajustar_orcamento(0.02, &mut hub);
|
| 340 |
+
// A-4: UCB1 com 3 braços
|
| 341 |
+
let mut u = controlador::Ucb1::novo(vec!["n1".into(), "n4_moe".into(), "n4".into()]);
|
| 342 |
+
for _ in 0..200 {
|
| 343 |
+
let braco = u.escolher();
|
| 344 |
+
let media = match braco { 0 => 0.35, 1 => 0.80, _ => 0.55 };
|
| 345 |
+
let r = (media + rng.normal(0.0, 0.06) as f64).clamp(0.0, 1.0);
|
| 346 |
+
u.registrar(braco, r);
|
| 347 |
+
}
|
| 348 |
+
let medias = u.medias();
|
| 349 |
+
let melhor = medias.iter().enumerate().max_by(|a, b| a.1.partial_cmp(b.1).unwrap()).map(|(i, _)| i).unwrap_or(0);
|
| 350 |
+
let d_ = serde_json::json!({
|
| 351 |
+
"versao": "autoajuste-0.6.0",
|
| 352 |
+
"tau_c_final": tau, "n_tentativas_final": n_ajustado,
|
| 353 |
+
"gate_final": g_final, "orcamento_b_final": b_ajustado,
|
| 354 |
+
"ajustes_auditados": hub.eventos_ajuste.len(),
|
| 355 |
+
"ucb1_medias": medias, "ucb1_melhor_braco": u.nomes[melhor],
|
| 356 |
+
"ucb1_arrependimento_medio": u.arrependimento_empirico() / 200.0,
|
| 357 |
+
"gates": {
|
| 358 |
+
"gate_a1_tau_quantil": (tau - 0.89).abs() < 0.08,
|
| 359 |
+
"gate_a3_n_reduzido": n_ajustado <= 8,
|
| 360 |
+
"gate_a5_gate_subiu": g_final > 0.0,
|
| 361 |
+
"gate_a2_b_finito": b_ajustado >= 1 && b_ajustado <= 16,
|
| 362 |
+
"gate_a4_ucb1_achou": u.nomes[melhor] == "n4_moe",
|
| 363 |
+
},
|
| 364 |
+
});
|
| 365 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 366 |
+
})
|
| 367 |
+
}
|
src/python/som.rs
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `python::som` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
/// Treina um 4D-SOM e devolve telemetria completa (EQ/TE/massa/cobertura).
|
| 8 |
+
#[pyfunction]
|
| 9 |
+
pub(crate) fn treinar_som4d(py: Python, dados: Vec<Vec<f64>>, cfg: Option<String>) -> PyResult<PyObject> {
|
| 10 |
+
let dev = candle_core::Device::Cpu;
|
| 11 |
+
let t = tensor_de_2d(dados, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 12 |
+
let mut c = som4d::CfgSom4D::default();
|
| 13 |
+
if let Some(js) = &cfg {
|
| 14 |
+
if let Ok(v) = serde_json::from_str::<serde_json::Value>(js) {
|
| 15 |
+
if let Some(o) = v.as_object() {
|
| 16 |
+
for (k, val) in o {
|
| 17 |
+
match k.as_str() {
|
| 18 |
+
"nx" => c.nx = val.as_u64().unwrap_or(4) as usize,
|
| 19 |
+
"ny" => c.ny = val.as_u64().unwrap_or(4) as usize,
|
| 20 |
+
"nz" => c.nz = val.as_u64().unwrap_or(4) as usize,
|
| 21 |
+
"nt" => c.nt = val.as_u64().unwrap_or(4) as usize,
|
| 22 |
+
"epocas" => c.epocas = val.as_u64().unwrap_or(50) as usize,
|
| 23 |
+
"sigma0" => c.sigma0 = val.as_f64().unwrap_or(2.0),
|
| 24 |
+
"sigma1" => c.sigma1 = val.as_f64().unwrap_or(0.5),
|
| 25 |
+
"eta0" => c.eta0 = val.as_f64().unwrap_or(0.5),
|
| 26 |
+
"eta1" => c.eta1 = val.as_f64().unwrap_or(0.05),
|
| 27 |
+
"semente" => c.semente = val.as_u64().unwrap_or(42),
|
| 28 |
+
"espalhada" => {
|
| 29 |
+
c.inicializacao = if val.as_bool().unwrap_or(false) {
|
| 30 |
+
som4d::Inicializacao4D::Espalhada
|
| 31 |
+
} else {
|
| 32 |
+
som4d::Inicializacao4D::Dados
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
_ => {}
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
let som = som4d::treinar_som4d(&t, &c, &dev)
|
| 42 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 43 |
+
let d = serde_json::json!({
|
| 44 |
+
"versao": som4d::versao(),
|
| 45 |
+
"celulas": som.n_celulas(),
|
| 46 |
+
"grade": [som.nx, som.ny, som.nz, som.nt],
|
| 47 |
+
"eq_inicial": som.eq_inicial,
|
| 48 |
+
"eq_final": som.eq_final,
|
| 49 |
+
"te_final": som.te_final,
|
| 50 |
+
"historico_eq_te": som.historia,
|
| 51 |
+
"massa_soma": som.massa.iter().sum::<f64>(),
|
| 52 |
+
});
|
| 53 |
+
Ok(pythonize::pythonize(py, &d)?.into())
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
/// Treina um GNG e devolve telemetria (vértices, arestas, componentes, EQ).
|
| 58 |
+
#[pyfunction]
|
| 59 |
+
pub(crate) fn treinar_gng(py: Python, dados: Vec<Vec<f64>>, cfg: Option<String>) -> PyResult<PyObject> {
|
| 60 |
+
let dev = candle_core::Device::Cpu;
|
| 61 |
+
let t = tensor_de_2d(dados, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 62 |
+
let mut c = som4d::CfgGng::default();
|
| 63 |
+
if let Some(js) = &cfg {
|
| 64 |
+
if let Ok(v) = serde_json::from_str::<serde_json::Value>(js) {
|
| 65 |
+
if let Some(o) = v.as_object() {
|
| 66 |
+
for (k, val) in o {
|
| 67 |
+
match k.as_str() {
|
| 68 |
+
"passos" => c.passos = val.as_u64().unwrap_or(4000) as usize,
|
| 69 |
+
"max_vertices" => c.max_vertices = val.as_u64().unwrap_or(60) as usize,
|
| 70 |
+
"lambda" => c.lambda = val.as_u64().unwrap_or(50) as usize,
|
| 71 |
+
"eps_b" => c.eps_b = val.as_f64().unwrap_or(0.05),
|
| 72 |
+
"eps_n" => c.eps_n = val.as_f64().unwrap_or(0.006),
|
| 73 |
+
"idade_max" => c.idade_max = val.as_u64().unwrap_or(50) as u32,
|
| 74 |
+
"semente" => c.semente = val.as_u64().unwrap_or(42),
|
| 75 |
+
_ => {}
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
let g = som4d::treinar_gng(&t, &c)
|
| 82 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 83 |
+
let d = serde_json::json!({
|
| 84 |
+
"versao": som4d::versao(),
|
| 85 |
+
"vertices": g.vertices.len(),
|
| 86 |
+
"arestas": g.arestas.len(),
|
| 87 |
+
"componentes_conexas": g.n_componentes_conexas(),
|
| 88 |
+
"insercoes": g.insercoes,
|
| 89 |
+
"eq_medio": g.eq_medio,
|
| 90 |
+
});
|
| 91 |
+
Ok(pythonize::pythonize(py, &d)?.into())
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
/// Treina um H-SOM e devolve a decomposição hierárquica (Teo H-1).
|
| 96 |
+
#[pyfunction]
|
| 97 |
+
pub(crate) fn treinar_hsom(py: Python, dados: Vec<Vec<f64>>, cfg: Option<String>) -> PyResult<PyObject> {
|
| 98 |
+
let dev = candle_core::Device::Cpu;
|
| 99 |
+
let t = tensor_de_2d(dados, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 100 |
+
let mut c = som4d::CfgHSom::default();
|
| 101 |
+
if let Some(js) = &cfg {
|
| 102 |
+
if let Ok(v) = serde_json::from_str::<serde_json::Value>(js) {
|
| 103 |
+
if let Some(o) = v.as_object() {
|
| 104 |
+
if let Some(x) = o.get("celulas_finas") {
|
| 105 |
+
c.celulas_finas = x.as_u64().unwrap_or(9) as usize;
|
| 106 |
+
}
|
| 107 |
+
if let Some(x) = o.get("epocas_finas") {
|
| 108 |
+
c.epocas_finas = x.as_u64().unwrap_or(20) as usize;
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
let h = som4d::treinar_hsom(&t, &c, &dev)
|
| 114 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 115 |
+
let cota = h.eq_grosso + 2.0 * h.r_fino + 2.0 * (h.eq_grosso * h.r_fino).sqrt();
|
| 116 |
+
let d = serde_json::json!({
|
| 117 |
+
"versao": som4d::versao(),
|
| 118 |
+
"eq_hierarquico": h.eq_hierarquico,
|
| 119 |
+
"eq_grosso": h.eq_grosso,
|
| 120 |
+
"r_fino": h.r_fino,
|
| 121 |
+
"cota_teo_h1": cota,
|
| 122 |
+
"cota_respeitada": h.eq_hierarquico <= cota + 1e-9,
|
| 123 |
+
});
|
| 124 |
+
Ok(pythonize::pythonize(py, &d)?.into())
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
/// Demo T-SOM: streaming com deriva controlada — rastreio (Teo T-1).
|
| 129 |
+
#[pyfunction]
|
| 130 |
+
pub(crate) fn demo_tsom(py: Python, dados_iniciais: Vec<Vec<f64>>, janela: Vec<Vec<f64>>, passos: usize, gamma: f64) -> PyResult<PyObject> {
|
| 131 |
+
let dev = candle_core::Device::Cpu;
|
| 132 |
+
let t0 = tensor_de_2d(dados_iniciais, &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 133 |
+
let cfg = som4d::CfgTSom {
|
| 134 |
+
cfg_grade: som4d::CfgSom4D { nx: 3, ny: 3, nz: 2, nt: 2, epocas: 15, ..Default::default() },
|
| 135 |
+
gamma,
|
| 136 |
+
janela: janela.len(),
|
| 137 |
+
};
|
| 138 |
+
let mut ts = som4d::TSom4D::novo(&t0, &cfg, &dev)
|
| 139 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 140 |
+
let mut historico = Vec::new();
|
| 141 |
+
for _ in 0..passos {
|
| 142 |
+
let eq = ts.passo(&janela);
|
| 143 |
+
historico.push(eq);
|
| 144 |
+
}
|
| 145 |
+
let d = serde_json::json!({
|
| 146 |
+
"versao": som4d::versao(),
|
| 147 |
+
"gamma": gamma,
|
| 148 |
+
"eq_temporal_historico": historico,
|
| 149 |
+
"t_passos": ts.t,
|
| 150 |
+
});
|
| 151 |
+
Ok(pythonize::pythonize(py, &d)?.into())
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
/// Demo/gate do auto-ajuste SOM com tetos (S-AB/S-SL).
|
| 156 |
+
#[pyfunction]
|
| 157 |
+
pub(crate) fn demo_autoajuste_som(py: Python) -> PyResult<PyObject> {
|
| 158 |
+
let mut a = controlador::AutoAjusteSom::novo(0.5, 1.0);
|
| 159 |
+
let (al_qe, _) = a.passo(1.0, 0.0, 0.0);
|
| 160 |
+
let (al_cross, sg_cross) = a.passo(0.0, 0.0, 1.0);
|
| 161 |
+
let (al_max, sg_max) = a.passo(1.0, 1.0, 1.0);
|
| 162 |
+
let mut parou_5chunks = false;
|
| 163 |
+
for _ in 0..5 {
|
| 164 |
+
parou_5chunks = a.avaliar_parada(0.95, 0.1, 0.1, false, true, false, false);
|
| 165 |
+
}
|
| 166 |
+
let d_ = serde_json::json!({
|
| 167 |
+
"versao": "som-autoadjust-0.7.0",
|
| 168 |
+
"alfa_qe_alta": al_qe, "alfa_crossing": al_cross, "sigma_crossing": sg_cross,
|
| 169 |
+
"alfa_max": al_max, "sigma_max": sg_max,
|
| 170 |
+
"parou_5chunks": parou_5chunks,
|
| 171 |
+
"motivo": a.motivo_parada,
|
| 172 |
+
"gates": {
|
| 173 |
+
"tetos_ok": al_max <= 1.0 + 1e-9 && sg_max <= 2.0 + 1e-9,
|
| 174 |
+
"piso_ok": al_cross >= 0.5 - 1e-9,
|
| 175 |
+
"qe_sobe": al_qe > 0.5,
|
| 176 |
+
"stop_dispara": parou_5chunks,
|
| 177 |
+
},
|
| 178 |
+
});
|
| 179 |
+
Ok(pythonize::pythonize(py, &d_)?.into())
|
| 180 |
+
}
|
src/python/treino_fases.rs
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! `python::treino_fases` — extraído do monólito na v0.10.0
|
| 2 |
+
//! (divisão SRP, crítica Qwen3.8 lote 3 nº 1; r-35 §2).
|
| 3 |
+
|
| 4 |
+
use super::*;
|
| 5 |
+
|
| 6 |
+
use super::nucleo::ConfigPy;
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
/// Treina a fase A (tokenizador). Devolve dict com resumo + telemetria.
|
| 10 |
+
#[pyfunction]
|
| 11 |
+
#[pyo3(signature = (cfg, sequencias, caminho_telemetria=None, caminho_checkpoint=None, callback=None, teto_seg=None))]
|
| 12 |
+
pub(crate) fn treinar_fase_a(
|
| 13 |
+
cfg: &ConfigPy,
|
| 14 |
+
sequencias: Vec<Vec<u32>>,
|
| 15 |
+
caminho_telemetria: Option<String>,
|
| 16 |
+
caminho_checkpoint: Option<String>,
|
| 17 |
+
callback: Option<Bound<'_, PyAny>>,
|
| 18 |
+
teto_seg: Option<f64>,
|
| 19 |
+
) -> PyResult<PyObject> {
|
| 20 |
+
let cb = callback_treino_de_py(callback);
|
| 21 |
+
let mut hub = telemetria::TelemetryHub::novo(caminho_telemetria);
|
| 22 |
+
let dados = treino::DadosTreino::novo(sequencias, cfg.cfg.modelo.comprimento_ctx);
|
| 23 |
+
let mut resumo = treino::treinar_fase_a(&cfg.cfg, &dados, &mut hub, &cb, caminho_checkpoint.as_deref(), teto_seg)
|
| 24 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("fase A: {e}")))?;
|
| 25 |
+
let _ = &mut resumo;
|
| 26 |
+
Python::with_gil(|py| {
|
| 27 |
+
let dados = serde_json::json!({
|
| 28 |
+
"resumo": {
|
| 29 |
+
"passos": resumo.passos, "perda_inicial": resumo.perda_inicial,
|
| 30 |
+
"perda_final": resumo.perda_final, "acc_recon": resumo.acc_recon,
|
| 31 |
+
"delta_ort_max": resumo.delta_ort_max,
|
| 32 |
+
"perplexidade_codebook": resumo.perplexidade_codebook,
|
| 33 |
+
"monotonia_prefixo": resumo.monotonia_prefixo,
|
| 34 |
+
"escore_aprendizado": resumo.escore_aprendizado,
|
| 35 |
+
},
|
| 36 |
+
"telemetria": hub.dump(),
|
| 37 |
+
});
|
| 38 |
+
Ok(pythonize::pythonize(py, &dados)?.into())
|
| 39 |
+
})
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
/// Treina a fase B (gerador) com tokenizador congelado carregado de checkpoint.
|
| 44 |
+
#[pyfunction]
|
| 45 |
+
#[pyo3(signature = (cfg, sequencias, caminho_tokenizador, caminho_telemetria=None, caminho_checkpoint=None, callback=None, teto_seg=None))]
|
| 46 |
+
pub(crate) fn treinar_fase_b(
|
| 47 |
+
cfg: &ConfigPy,
|
| 48 |
+
sequencias: Vec<Vec<u32>>,
|
| 49 |
+
caminho_tokenizador: &str,
|
| 50 |
+
caminho_telemetria: Option<String>,
|
| 51 |
+
caminho_checkpoint: Option<String>,
|
| 52 |
+
callback: Option<Bound<'_, PyAny>>,
|
| 53 |
+
teto_seg: Option<f64>,
|
| 54 |
+
) -> PyResult<PyObject> {
|
| 55 |
+
let cb = callback_treino_de_py(callback);
|
| 56 |
+
let mut hub = telemetria::TelemetryHub::novo(caminho_telemetria);
|
| 57 |
+
let dados = treino::DadosTreino::novo(sequencias, cfg.cfg.modelo.comprimento_ctx);
|
| 58 |
+
// carrega o tokenizador congelado
|
| 59 |
+
let dev = candle_core::Device::Cpu;
|
| 60 |
+
let mut tok = tokenizador::TokenizadorConceitos::novo(&cfg.cfg, &dev)
|
| 61 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("construção: {e}")))?;
|
| 62 |
+
carregar_tokenizador(&mut tok, caminho_tokenizador)
|
| 63 |
+
.map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
|
| 64 |
+
let resumo = treino::treinar_fase_b(&cfg.cfg, &tok, &dados, &mut hub, &cb, caminho_checkpoint.as_deref(), teto_seg)
|
| 65 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("fase B: {e}")))?;
|
| 66 |
+
Python::with_gil(|py| {
|
| 67 |
+
let dados = serde_json::json!({
|
| 68 |
+
"resumo": {
|
| 69 |
+
"passos": resumo.passos, "perda_inicial": resumo.perda_inicial,
|
| 70 |
+
"perda_final": resumo.perda_final, "acc_conceitos": resumo.acc_recon,
|
| 71 |
+
"escore_aprendizado": resumo.escore_aprendizado,
|
| 72 |
+
},
|
| 73 |
+
"telemetria": hub.dump(),
|
| 74 |
+
});
|
| 75 |
+
Ok(pythonize::pythonize(py, &dados)?.into())
|
| 76 |
+
})
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
pub(crate) fn carregar_tokenizador(tok: &mut tokenizador::TokenizadorConceitos, caminho: &str) -> Result<(), String> {
|
| 81 |
+
let txt = std::fs::read_to_string(caminho).map_err(|e| format!("{e}"))?;
|
| 82 |
+
let v: serde_json::Value = serde_json::from_str(&txt).map_err(|e| format!("{e}"))?;
|
| 83 |
+
let obj = v.as_object().ok_or("formato de checkpoint inválido")?;
|
| 84 |
+
let vars = tok.vars();
|
| 85 |
+
for (i, var) in vars.iter().enumerate() {
|
| 86 |
+
if let Some(entrada) = obj.get(&format!("v{i}")) {
|
| 87 |
+
let forma: Vec<usize> = entrada["forma"].as_array().map(|a| a.iter().filter_map(|x| x.as_u64().map(|y| y as usize)).collect()).unwrap_or_default();
|
| 88 |
+
let dados: Vec<f32> = entrada["dados"].as_array().map(|a| a.iter().filter_map(|x| x.as_f64().map(|y| y as f32)).collect()).unwrap_or_default();
|
| 89 |
+
if forma.is_empty() || dados.is_empty() {
|
| 90 |
+
continue;
|
| 91 |
+
}
|
| 92 |
+
let dev = candle_core::Device::Cpu;
|
| 93 |
+
let t2 = candle_core::Tensor::from_vec(dados, forma.clone(), &dev)
|
| 94 |
+
.and_then(|t| t.reshape(forma))
|
| 95 |
+
.map_err(|e| format!("{e}"))?;
|
| 96 |
+
var.set(&t2).map_err(|e| format!("{e}"))?;
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
Ok(())
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
/// Gera amostras com NFE = L e devolve textos decodificados (modo conceitos).
|
| 104 |
+
#[pyfunction]
|
| 105 |
+
#[pyo3(signature = (cfg, caminho_tokenizador, caminho_gerador, n_amostras=2, parar_no_nivel=None, greedy=true))]
|
| 106 |
+
pub(crate) fn gerar_amostras(
|
| 107 |
+
cfg: &ConfigPy,
|
| 108 |
+
caminho_tokenizador: &str,
|
| 109 |
+
caminho_gerador: &str,
|
| 110 |
+
n_amostras: usize,
|
| 111 |
+
parar_no_nivel: Option<usize>,
|
| 112 |
+
greedy: bool,
|
| 113 |
+
) -> PyResult<PyObject> {
|
| 114 |
+
let dev = candle_core::Device::Cpu;
|
| 115 |
+
let mut tok = tokenizador::TokenizadorConceitos::novo(&cfg.cfg, &dev)
|
| 116 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 117 |
+
carregar_tokenizador(&mut tok, caminho_tokenizador)
|
| 118 |
+
.map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
|
| 119 |
+
let mut ger = gerador::GeradorConceitos::novo(&cfg.cfg, &dev)
|
| 120 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 121 |
+
carregar_gerador(&mut ger, caminho_gerador)
|
| 122 |
+
.map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
|
| 123 |
+
let mut amostrador = amostrador::AmostradorConceitos::novo(
|
| 124 |
+
&mut ger, &tok, cfg.cfg.ncm.cfg_scale, cfg.cfg.ncm.temperaturas.limites, cfg.cfg.ncm.temperaturas.entropia_alvo,
|
| 125 |
+
);
|
| 126 |
+
let mut rng = <rand::prelude::StdRng as rand::SeedableRng>::seed_from_u64(4242);
|
| 127 |
+
let inicio = std::time::Instant::now();
|
| 128 |
+
let (codigos, nfe) = amostrador.gerar(n_amostras.max(1), &dev, &mut rng, parar_no_nivel, greedy)
|
| 129 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("amostragem: {e}")))?;
|
| 130 |
+
let tempo_ms = inicio.elapsed().as_secs_f64() * 1000.0;
|
| 131 |
+
// decodifica: ẑ final → decoder AR greedy → tokens
|
| 132 |
+
let nivel_final = codigos.len();
|
| 133 |
+
let zhats = tok.reconstruir_de_codigos(&codigos, nivel_final, n_amostras.max(1), &dev)
|
| 134 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 135 |
+
let textos_ids = decodificar_textos(&tok, zhats.last(), n_amostras.max(1))
|
| 136 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
|
| 137 |
+
Python::with_gil(|py| {
|
| 138 |
+
let d = serde_json::json!({
|
| 139 |
+
"nfe": nfe, "l": cfg.cfg.l_niveis(), "tempo_ms": tempo_ms,
|
| 140 |
+
"niveis_usados": nivel_final, "ids_tokens": textos_ids,
|
| 141 |
+
"temperaturas": amostrador.temperaturas,
|
| 142 |
+
});
|
| 143 |
+
Ok(pythonize::pythonize(py, &d)?.into())
|
| 144 |
+
})
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
fn decodificar_textos(tok: &tokenizador::TokenizadorConceitos, zhat: Option<&candle_core::Tensor>, b: usize) -> Result<Vec<Vec<u32>>, String> {
|
| 149 |
+
let Some(z) = zhat else { return Ok(vec![vec![]; b]) };
|
| 150 |
+
tok.decodificar_livre(z, 48).map_err(|e| format!("{e}"))
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
pub(crate) fn carregar_gerador(ger: &mut gerador::GeradorConceitos, caminho: &str) -> Result<(), String> {
|
| 155 |
+
let txt = std::fs::read_to_string(caminho).map_err(|e| format!("{e}"))?;
|
| 156 |
+
let v: serde_json::Value = serde_json::from_str(&txt).map_err(|e| format!("{e}"))?;
|
| 157 |
+
let obj = v.as_object().ok_or("formato inválido")?;
|
| 158 |
+
let vars = ger.vars();
|
| 159 |
+
for (i, var) in vars.iter().enumerate() {
|
| 160 |
+
if let Some(entrada) = obj.get(&format!("v{i}")) {
|
| 161 |
+
let forma: Vec<usize> = entrada["forma"].as_array().map(|a| a.iter().filter_map(|x| x.as_u64().map(|y| y as usize)).collect()).unwrap_or_default();
|
| 162 |
+
let dados: Vec<f32> = entrada["dados"].as_array().map(|a| a.iter().filter_map(|x| x.as_f64().map(|y| y as f32)).collect()).unwrap_or_default();
|
| 163 |
+
if forma.is_empty() || dados.is_empty() {
|
| 164 |
+
continue;
|
| 165 |
+
}
|
| 166 |
+
let dev = candle_core::Device::Cpu;
|
| 167 |
+
let t = candle_core::Tensor::from_vec(dados, forma.clone(), &dev)
|
| 168 |
+
.and_then(|t| t.reshape(forma))
|
| 169 |
+
.map_err(|e| format!("{e}"))?;
|
| 170 |
+
var.set(&t).map_err(|e| format!("{e}"))?;
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
Ok(())
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
/// **FASE C — treino conjunto Pensamento↔backbone** (dados reais).
|
| 178 |
+
#[pyfunction]
|
| 179 |
+
pub(crate) fn treinar_fase_c(
|
| 180 |
+
cfg: &ConfigPy,
|
| 181 |
+
sequencias: Vec<Vec<u32>>,
|
| 182 |
+
caminho_tokenizador: &str,
|
| 183 |
+
passos: usize,
|
| 184 |
+
caminho_gerador: Option<String>,
|
| 185 |
+
caminho_telemetria: Option<String>,
|
| 186 |
+
callback: Option<Bound<'_, PyAny>>,
|
| 187 |
+
teto_seg: Option<f64>,
|
| 188 |
+
) -> PyResult<PyObject> {
|
| 189 |
+
let cb = callback_treino_de_py(callback);
|
| 190 |
+
let mut hub = telemetria::TelemetryHub::novo(caminho_telemetria);
|
| 191 |
+
let dados = treino::DadosTreino::novo(sequencias, cfg.cfg.modelo.comprimento_ctx);
|
| 192 |
+
let dev = candle_core::Device::Cpu;
|
| 193 |
+
let mut tok = tokenizador::TokenizadorConceitos::novo(&cfg.cfg, &dev)
|
| 194 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("construção: {e}")))?;
|
| 195 |
+
carregar_tokenizador(&mut tok, caminho_tokenizador)
|
| 196 |
+
.map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
|
| 197 |
+
let resumo = treino::treinar_fase_c(&cfg.cfg, &tok, &dados, &mut hub, &cb, teto_seg, passos, caminho_gerador.as_deref())
|
| 198 |
+
.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("fase C: {e}")))?;
|
| 199 |
+
Python::with_gil(|py| {
|
| 200 |
+
let dados = serde_json::json!({
|
| 201 |
+
"resumo": {
|
| 202 |
+
"passos": resumo.passos,
|
| 203 |
+
"ce_inicial": resumo.ce_inicial,
|
| 204 |
+
"ce_final": resumo.ce_final,
|
| 205 |
+
"ce_gatilhada_sem_correcao": resumo.ce_gatilhada_sem_correcao,
|
| 206 |
+
"ce_gatilhada_com_correcao": resumo.ce_gatilhada_com_correcao,
|
| 207 |
+
"confianca_media_inicial": resumo.confianca_media_inicial,
|
| 208 |
+
"confianca_media_final": resumo.confianca_media_final,
|
| 209 |
+
"confianca_pos_correcao": resumo.confianca_pos_correcao,
|
| 210 |
+
"taxa_gatilho": resumo.taxa_gatilho,
|
| 211 |
+
"ece_antes": resumo.ece_antes,
|
| 212 |
+
"ece_depois": resumo.ece_depois,
|
| 213 |
+
"temperatura_calibrada": resumo.temperatura_calibrada,
|
| 214 |
+
"tau_c": resumo.tau_c,
|
| 215 |
+
"taxa_conflito_pcgrad": resumo.taxa_conflito_pcgrad,
|
| 216 |
+
"decaimento_delta": resumo.decaimento_delta,
|
| 217 |
+
"acordos_medios": resumo.acordos_medios,
|
| 218 |
+
"escore_aprendizado": resumo.escore_aprendizado,
|
| 219 |
+
"rodadas_acoplador": resumo.rodadas_acoplador,
|
| 220 |
+
"timeouts_acoplador": resumo.timeouts_acoplador,
|
| 221 |
+
"maior_sobreposicao_us": resumo.maior_sobreposicao_us,
|
| 222 |
+
},
|
| 223 |
+
"painel_v060": hub.painel_v060(),
|
| 224 |
+
"telemetria": hub.dump(),
|
| 225 |
+
});
|
| 226 |
+
Ok(pythonize::pythonize(py, &dados)?.into())
|
| 227 |
+
})
|
| 228 |
+
}
|