diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..853ccd1110f107903097ea968b8c47154332c5c5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +graficos/graf_ram.png filter=lfs diff=lfs merge=lfs -text diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..beaa0d35abdd7cc2e6d235cb88879dc195e67cc3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Projeto KHTST + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..eb1ea7ebc60d3f0673bd61a44b702d01f8e058bb --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# KHTST — modelo multimodal PT-BR (v8) + +KHTST é a evolução do projeto AURORA (renomeação integral AURORA → KHTST, +pedido do projeto) — um modelo multimodal compacto para PT-BR com +roteamento por S-SOM, punição SGDR, punição de novidade restrita a empates +de Voronoi, controle Reward/Punishment/Penalty (RPP) e **janela de contexto +256K tokens** (compressão indexada, doc 13). + +**14,89M parâmetros · vocab 16384 · 9 tarefas (lm/noticia/pontuacao/ +instrucao/tts/vqa/ocr/imagem_caption/asr) · encoders imagem/áudio/vídeo.** + +> ⚠️ **STATUS DO GATE DE QUALIDADE (honesto): FALHOU.** +> Nesta corrida, o KHTST NÃO superou nem empatou o AURORA nas métricas +> (`ppl_lm` 2542,11 vs 17,09 do baseline publicado). Conforme a regra do +> projeto, **os estados (pesos) do modelo NÃO foram publicados** — este +> repositório publica o CÓDIGO completo (scripts de alta qualidade, +> provas matemáticas e evidências) e o diagnóstico que corrige a causa +> raiz. Um retreino limpo com a política corrigida está pronto para +> executar (est. ~3,3 h; o teto de 5 h da sessão impediu refazer). + +## O que há de novo no v8 (itens a–j, com provas em `docs/matematica/19`) + +| item | melhoria | onde | prova | +|---|---|---|---| +| a | micro buffers anulares em redes recorrentes (0 alocação/passo no decode) | `percepcao/microunidades.py` | Teo 19.6 | +| b | canal adhoc I/O + **stop/continue** durante solicitações ativas | `servico/adhoc.py`, `scripts/09_teste_item_b.py` | Teos 19.14–19.16 | +| c | quantização 8-bit seletiva matematicamente vantajosa (torchao/bnb) | `quanta/quantizacao.py` §10 | Teo 19.18 | +| d | anti-vanishing: Leaky ReLU adaptativa + pesos de árvore bidirecionais + skip neutro + BatchNorm | `microunidades.py` | Teos 19.7–19.10 | +| e | map-reduce `torch.vmap`/`sum`/`mean` nos experts MoE | `percepcao/moe.py` | Teo 19.11 (diff 0,0) | +| f | atenção **árvore bidirecional fora de ordem**; raio_janela e kv_max **×4** (128/2048); `AtencaoCabecas` | `percepcao/atencao.py` | Teos 19.1–19.4 | +| g | MoE **2 encoders + 4 decoders** fora de ordem agrupável/desagrupável | `percepcao/moe.py` | Teos 19.12–19.13 | +| h | gestão de memória RAM/armazenamento com limiar adaptativo | `telemetria/gestor_memoria.py` | Teo 19.17 | +| i | testes em dados reais → 6 bugs latentes do v7 corrigidos (ver abaixo) | vários | — | +| j | parâmetros auto-ajustáveis matematicamente + provas conjuntas | `treino/treinador.py` | Teos 19.26–19.27 | + +**ViT (evolução de `video.py`/`blocos.py`/`microunidades.py`):** Token +Merging (ToMe) com casamento bipartido único e auto-ajuste da fração de +fusão; **LRA-QViT** com RB-LRA, WADS e STE — ramificações **1-bit ∥ 4-bit +em paralelo** detectando **direção vetorial** da iluminação (unitária) e +**intensidade**, mapas 2-D de contorno/área; critério de vantagem da +difusão (`vantagem_difusao`). Provas: Teos 19.19–19.24, 19.28. + +**Cython + C/C++:** núcleos `acelerado/nucleos.pyx` (Kohonen sequencial + +entropia/punição) com semântica sequencial exata (Teo 19.25) — **speedup +medido 17,89×**, fallback puro-torch idêntico. + +## Verificações (tudo verde) + +- `tests/test_khtst.py` … `test_khtst_v6.py` (herdadas): 25+36+37+29+48 ✓ +- `tests/test_khtst_v7.py` (nova, Teoremas 19.x): **44 ✓ · 0 ✗** +- **Item (b) no modelo treinado: APROVADO** — 4 requisições recebidas + ANTES da 1ª resposta, processamento serial, stop (cancelamento) e + pause→continue honrados em ≤ 1 passo (`teste_item_b_v8.json`). + +## Treino desta corrida (dentro do teto de 5 h) + +| métrica | valor | +|---|---| +| passos | 3192 (8 épocas + DPO 93 passos acc 1,0 + consolidação SOM) | +| tempo acumulado | **3,98 h** (teto 5 h respeitado; sem treino em background) | +| RAM | pico 3358 MB · média 2524 MB · mín 429 MB (5919 amostras) | +| corpus | 3548 registros PT-BR reais (18 fontes HF; 987 multimodais) | +| gate de não-regressão | **FALHOU** — `ppl_lm` 2542,11 vs 17,09 (regressão registrada em `comparacao_treino_v8.json`) | + +## Diagnóstico da regressão (item i — bugs latentes do v7 encontrados e corrigidos) + +1. **Reinícios SGDR espúrios**: 16 warm restarts em 2075 passos — o + cooldown (`janela/2`) era menor que o próprio ciclo (T₀) e a cláusula 1 + da eq. 9.3 disparava no passo 9 (sem guarda de aquecimento). O lr passou + 43% do tempo no piso ⇒ sub-convergência. **Correção** (`treino/punicao.py`): + cooldown = T₀ e guarda `passo ≤ warmup + T₀` para AMBAS as cláusulas. +2. **Gate de não-regressão inoperante no v7**: as chaves do gate eram + `ppl_valid/perda_valid`, mas as métricas reais são `ppl_lm/perda_media_final` + — o "aprovado" do v7 nunca verificou o ppl. **Correção** (`scripts/08`): + chaves corretas ⇒ o gate agora reprova de verdade (como acima). +3. Outros: einsum de decode `bhte→bhe` (quebrava TODA geração com cache no v7), + typo `perla→perda`, `NameError` no script 07, clamp de rótulos do S-SOM + (n_classes=8 × 9 tarefas), resumos v4/v6/v7 divergentes entre scripts. + +## Reprodução + +```bash +python3 scripts/01_verificar_ambiente.py +python3 scripts/02_coletar_corpus.py # retomável por fonte +python3 scripts/03_treinar_tokenizador.py +python3 scripts/04_treinar.py --orcamento 470 --teto_horas 5.0 +python3 scripts/05_avaliar.py --sem_difusao +python3 scripts/09_teste_item_b.py # item (b) com o modelo real +python3 scripts/08_graficos_card.py # gráficos + gate +python3 scripts/06_publicar_hf.py # commit único (gate bloqueia estados) +``` + +## Estrutura + +`src/khtst/` — núcleo, percepção (atenção/microunidades/moe/vit_lra), +memória (S-SOM + 8 variantes + janela 256K), treino (4 fases + RPP + SGDR), +servico (canal adhoc), quanta (8-bit + STE), telemetria (hub + gestor de +memória), qualidade (Agente Engenheiro), acelerado (Cython+C). +`docs/matematica/00–19` — fundamentos, teoremas e provas. +`graficos/` — desempenho desta corrida (apenas no card). + +## Licença + +MIT (herdada do projeto). diff --git a/avaliacao_v7.json b/avaliacao_v7.json new file mode 100644 index 0000000000000000000000000000000000000000..344dcca3ac61630300722d3c72f63005201deef4 --- /dev/null +++ b/avaliacao_v7.json @@ -0,0 +1,938 @@ +{ + "estado": "fase-som", + "roteador_ssom": { + "amostras": 1536, + "pronto": true, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.0, + "conf_media": 0.4917, + "frac_rotas_ativas": 0.5312, + "k": 24, + "taxa_ativos": 1.0 + }, + "perdas_tarefa": { + "lm": { + "treinado": 2.8384029467900596, + "aleatorio": 9.732619444529215 + }, + "noticia": { + "treinado": 2.5535371700922647, + "aleatorio": 9.723073641459147 + }, + "instrucao": { + "treinado": 2.035900592803955, + "aleatorio": 9.758591492970785 + }, + "pontuacao": { + "treinado": 2.648297905921936, + "aleatorio": 9.727588017781576 + }, + "imagem_caption": { + "treinado": 2.127371827761332, + "aleatorio": 9.717129548390707 + }, + "vqa": { + "treinado": 2.177152613798777, + "aleatorio": 9.693151950836182 + }, + "ocr": { + "treinado": 1.418007344007492, + "aleatorio": 9.757900714874268 + }, + "asr": { + "treinado": 2.1221489111582437, + "aleatorio": 9.733868916829428 + }, + "tts": { + "treinado": 1.675029953320821, + "aleatorio": 9.691224416097006 + } + }, + "ppl_lm": 17.088452564794597, + "ppl_lm_aleatorio": 16858.65484439469, + "moe_uso_por_tarefa": { + "lm": [ + { + "experts_top": [ + 5, + 4 + ], + "grupo_top": [ + 2, + 2 + ] + }, + { + "experts_top": [ + 0, + 1 + ], + "grupo_top": [ + 0, + 0 + ] + } + ], + "vqa": [ + { + "experts_top": [ + 2, + 3 + ], + "grupo_top": [ + 1, + 1 + ] + }, + { + "experts_top": [ + 5, + 4 + ], + "grupo_top": [ + 2, + 2 + ] + } + ], + "ocr": [ + { + "experts_top": [ + 2, + 3 + ], + "grupo_top": [ + 1, + 1 + ] + }, + { + "experts_top": [ + 5, + 4 + ], + "grupo_top": [ + 2, + 2 + ] + } + ], + "asr": [ + { + "experts_top": [ + 0, + 1 + ], + "grupo_top": [ + 0, + 0 + ] + }, + { + "experts_top": [ + 2, + 3 + ], + "grupo_top": [ + 1, + 1 + ] + } + ], + "tts": [ + { + "experts_top": [ + 5, + 4 + ], + "grupo_top": [ + 2, + 2 + ] + }, + { + "experts_top": [ + 0, + 1 + ], + "grupo_top": [ + 0, + 0 + ] + } + ] + }, + "mtp": { + "alphas": [ + 0.9997844099998474, + 0.00021563710470218211 + ], + "per_cabeca": [ + 2.9869613647460938, + 10.111918449401855 + ], + "entropia": 0.00203594658523798, + "termos_ce": 0.0 + }, + "w8a8_delta": 0.0011706352233886719, + "som_treinado": { + "variante_ativa": "cpn", + "variantes": { + "som": { + "k": 24, + "taxa_ativos": 1.0, + "resemeados": 24, + "usos": 256, + "atingiu_alvo": true + }, + "gg": { + "k": 4, + "taxa_ativos": 1.0, + "resemeados": 4, + "usos": 256, + "atingiu_alvo": true + }, + "gcs": { + "k": 3, + "arestas": 3, + "eq_treino": 88415.8203125, + "atingiu_alvo": true + }, + "gsom": { + "k": 29, + "gt": 1.05, + "erro_medio": 0.0, + "atingiu_alvo": true + }, + "hsom": {}, + "tkm": { + "k": 32, + "taxa_ativos": 1.0, + "resemeados": 32, + "usos": 256, + "atingiu_alvo": true + }, + "rsom": { + "k": 32, + "taxa_ativos": 1.0, + "resemeados": 32, + "usos": 256, + "atingiu_alvo": true + }, + "cpn": {}, + "ssom": { + "erro": "IndexError: index 8 is out of bounds for dimension 0 with size 8" + } + }, + "roteador": { + "amostras": 1586, + "pronto": true, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.032477, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 1.0 + }, + "invariante_sem_orfaos": true, + "orfaos_por_variante": { + "som": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "gg": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "gcs": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "gsom": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "hsom": { + "orfaos": 0, + "relocados": 0 + }, + "tkm": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "rsom": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "cpn": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "ssom": { + "orfaos": 0, + "relocados": 23, + "taxa_ativos": 1.0 + } + } + }, + "som_eq_fresco": { + "som": { + "taxa_ativos": 1.0, + "eq": 75.31108856201172 + }, + "gg": { + "taxa_ativos": 1.0, + "eq": 144.36021423339844 + }, + "gcs": { + "taxa_ativos": 1.0, + "eq": 160.38645935058594 + }, + "gsom": { + "taxa_ativos": 1.0, + "eq": 130.51382446289062 + }, + "tkm": { + "taxa_ativos": 1.0, + "eq": 72.5010757446289 + }, + "rsom": { + "taxa_ativos": 1.0, + "eq": 75.71780395507812 + }, + "ssom": { + "taxa_ativos": 1.0, + "eq": 78.7991714477539 + } + }, + "geracao": { + "sem_punicao": "ricos a a a a Atualadawaficados novo 75 cada motivação o choque o clima inédentpresenta competimes tipo individual-sucedidos observador", + "com_punicao": " NaNTde a testevem trromcada 360 Bat que globais lou topo atingmadas atadado desa Emb 89 con badminton dizie sua simples doenças na" + }, + "microunidades": { + "lm": [ + { + "ramos": [ + "conv_dil", + "gru", + "mlp" + ], + "alpha_medio": [ + 0.2641, + 0.305, + 0.4309 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "alpha_medio": [ + 0.5519, + 0.1535, + 0.1445, + 0.1501 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "alpha_medio": [ + 0.2203, + 0.2562, + 0.2834, + 0.2401 + ], + "passos_rec": 2 + } + ], + "vqa": [ + { + "ramos": [ + "conv_dil", + "gru", + "mlp" + ], + "alpha_medio": [ + 0.0647, + 0.2521, + 0.6833 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "alpha_medio": [ + 0.2515, + 0.2591, + 0.2497, + 0.2398 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "alpha_medio": [ + 0.0645, + 0.1988, + 0.4256, + 0.311 + ], + "passos_rec": 2 + } + ], + "asr": [ + { + "ramos": [ + "conv_dil", + "gru", + "mlp" + ], + "alpha_medio": [ + 0.2777, + 0.3308, + 0.3916 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "alpha_medio": [ + 0.5948, + 0.1387, + 0.1316, + 0.1348 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "alpha_medio": [ + 0.1736, + 0.2893, + 0.2814, + 0.2557 + ], + "passos_rec": 2 + } + ], + "instrucao": [ + { + "ramos": [ + "conv_dil", + "gru", + "mlp" + ], + "alpha_medio": [ + 0.1276, + 0.2803, + 0.5922 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "alpha_medio": [ + 0.353, + 0.2125, + 0.2194, + 0.2151 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "alpha_medio": [ + 0.0723, + 0.2226, + 0.4016, + 0.3036 + ], + "passos_rec": 2 + } + ] + }, + "treino_prs_v8": { + "trust": 0.4091, + "tau": 0.55736, + "clip": 3.2906, + "boost": 1.0, + "streak": 0, + "ciclos_sgdr": 0, + "recompensa_acum": 17.593, + "beta_min_relativo": 2.0 + }, + "treino_confianca": { + "h_ema": 0.6650000214576721, + "ece": 0.3813131313131314, + "posterior_media": 0.32673267326732675, + "bernstein": [ + 0.176243817077732, + 0.4772215294569215 + ] + }, + "treino_memoria": { + "slots": 25, + "capacidade": 64, + "escritas": 25, + "rejeitadas_confianca": 0, + "hit_rate": 1.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "treino_crescimento": [], + "memoria_execucao": { + "slots": 32, + "capacidade": 32, + "escritas": 45, + "rejeitadas_confianca": 0, + "hit_rate": 1.0, + "comprimidos": 32, + "entropia_codebook": 3.5029096603393555, + "perda_vq": "tensor(7.0846)", + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375, + "escritas_aceitas": 45, + "consultas_com_acerto": 15, + "comprimidos_agora": 32 + }, + "curva_epocas": [ + { + "epoca": 0, + "perda_media": 4.501659254233043, + "ppl_lm": 116.51917431950382 + }, + { + "epoca": 1, + "perda_media": 2.1522662341594696, + "ppl_lm": 20.32702512440419 + }, + { + "epoca": 2, + "perda_media": 1.628365287516499, + "ppl_lm": 6.841325508316532 + }, + { + "epoca": 3, + "perda_media": 1.5996007979906435, + "ppl_lm": 6.208188231846149 + }, + { + "epoca": 4, + "perda_media": 1.4905521827482286, + "ppl_lm": 5.169895857934865 + }, + { + "epoca": 5, + "perda_media": 0.9951962381601334, + "ppl_lm": 3.663949358479845 + }, + { + "epoca": 5, + "perda_media": 0.78661770003876, + "ppl_lm": 2.2905761511962 + } + ], + "coerencia": { + "corpus_25pct": { + "n_registros": 887, + "repeticao_pct": 46.32, + "ancoragem_bigramas": 0.0, + "entropia_logits": 3.0155 + }, + "corpus_50pct": { + "n_registros": 1774, + "repeticao_pct": 63.97, + "ancoragem_bigramas": 0.0071, + "entropia_logits": 3.0155 + }, + "corpus_100pct": { + "n_registros": 3548, + "repeticao_pct": 52.94, + "ancoragem_bigramas": 0.0, + "entropia_logits": 3.0155 + }, + "amostras": [ + " corpos a na china oran melhorar incêndiocas tenha controlaroumentos tradicional ideias tr 26un agachamentos fornecer em meadosvosvosÉ pela ", + " afastados dança melhorar sexove por fatoresun agachamentosense em bicicleta retorno Isso cargo de a a na Califórnia tor Os benefícios passa", + " eie Este comJánas tipo passa ritmo tropical02 de", + "" + ] + }, + "clip_score": { + "clip_real": -0.1604, + "clip_controle": -0.1702, + "margem": 0.0098, + "n": 32 + }, + "itm": { + "acuracia": 0.5, + "f1": 0.0, + "n": 16, + "tp": 0, + "fp": 0, + "fn": 8 + }, + "ppl_visual": { + "ppl_visual": 1.16, + "n": 192, + "codigos_distintos": 8 + }, + "ppl_multimodal": { + "ppl_mm": 9.41, + "ce_mm": 2.2417, + "n": 2733 + }, + "geracao_metricas": { + "medias": { + "bleu": 0.0, + "rouge_l": 0.0149, + "meteor": 0.0, + "cider": 0.0001 + }, + "n": 12, + "amostras": [ + { + "hipotese": " a a a sombra levando levando ao emn19 indes incêndio", + "ref": "Na foto podemos ver um homem centralizado na imagem, posicio", + "bleu": 0.0, + "rouge_l": 0.023288, + "meteor": 0.0, + "cider": 0.0 + }, + { + "hipotese": " a gordura levando sociedade formação ajudar tropicalgou como pode levar sozinha", + "ref": "Na foto podemos ver duas pessoas sobre uma prancha de stand ", + "bleu": 0.0, + "rouge_l": 0.014975, + "meteor": 0.0, + "cider": 5.8e-05 + }, + { + "hipotese": "", + "ref": "Na foto podemos ver cinco pessoas reunidas ao redor de uma c", + "bleu": 0.0, + "rouge_l": 0.0, + "meteor": 0.0, + "cider": 0.0 + }, + { + "hipotese": " trânsito PL rastre!1540 conelos a a competiçãodam várias United físicoifúvemerc", + "ref": "Na imagem podemos ver um homem usando uma jaqueta jeans de m", + "bleu": 0.0, + "rouge_l": 0.018564, + "meteor": 0.0, + "cider": 0.0 + } + ] + }, + "benchmarks": { + "mmmu": { + "benchmark": "MMMU-proxy-PT", + "n": 30, + "acuracia": 0.0, + "acertos": 0, + "distribuicao": { + "∅": 27, + "A": 3 + }, + "acaso": 0.25, + "rotulo": "proxy em escala reduzida" + }, + "mme": { + "benchmark": "MME-proxy-PT", + "n": 15, + "acc": 0.0, + "acc_mais": 0.0, + "score_mme": 0.0, + "rotulo": "proxy em escala reduzida" + }, + "mathvista": { + "benchmark": "MathVista-proxy-PT", + "n": 2, + "acuracia": 0.0, + "acertos": 0, + "rotulo": "proxy em escala reduzida" + } + }, + "som_expandido": { + "som": { + "k": 24, + "qe": 59.763092, + "te": 0.980469, + "distorcao": 4559.125, + "sigma0": 3.0, + "sigma_f": 0.4, + "alpha0": 0.5, + "alpha_ultimo": 0.49505, + "n_ativos": 24, + "entropia": 3.178054, + "rank_efetivo": 24.0, + "frac_vivas": 1.0, + "u_matrix_resumo": { + "u_media": 32.043781, + "u_max": 60.584747, + "u_std": 13.450652, + "fronteiras_picos": 5 + }, + "taxa_ativos_ema": 1.0 + }, + "gg": { + "k": 4, + "qe": 824.069397, + "te": 0.316406, + "distorcao": 2294.106445, + "sigma0": 3.0, + "sigma_f": 0.4, + "alpha0": 0.5, + "alpha_ultimo": 0.49505, + "n_ativos": 4, + "entropia": 1.386294, + "rank_efetivo": 4.0, + "frac_vivas": 1.0, + "u_matrix_resumo": { + "u_media": 11.976965, + "u_max": 13.957561, + "u_std": 1.774012, + "fronteiras_picos": 1 + }, + "taxa_ativos_ema": 1.0 + }, + "gcs": { + "k": 3, + "qe": 250.228699, + "te": 1.0, + "distorcao": 1513.247803, + "sigma0": 0.0, + "sigma_f": 0.0, + "alpha0": 0.0, + "alpha_ultimo": 0.0, + "n_ativos": 2, + "entropia": 0.574847, + "rank_efetivo": 1.7769, + "frac_vivas": 0.6667, + "u_matrix_resumo": { + "u_media": 0.0, + "u_max": 0.0, + "u_std": 0.0, + "fronteiras_picos": 0 + }, + "taxa_ativos_ema": null + }, + "gsom": { + "k": 4, + "qe": 220.440918, + "te": 0.410156, + "distorcao": 3028.990967, + "sigma0": 3.0, + "sigma_f": 0.4, + "alpha0": 0.5, + "alpha_ultimo": 0.49505, + "n_ativos": 4, + "entropia": 1.386294, + "rank_efetivo": 4.0, + "frac_vivas": 1.0, + "u_matrix_resumo": { + "u_media": 36.250061, + "u_max": 60.509567, + "u_std": 19.814838, + "fronteiras_picos": 1 + }, + "taxa_ativos_ema": 1.0 + }, + "hsom": { + "k": 12, + "qe": 71.016319, + "te": 0.855469, + "distorcao": 3511.441895, + "sigma0": 2.5, + "sigma_f": 0.4, + "alpha0": 0.5, + "alpha_ultimo": 0.49505, + "n_ativos": 12, + "entropia": 2.484907, + "rank_efetivo": 12.0, + "frac_vivas": 1.0, + "u_matrix_resumo": { + "u_media": 28.268629, + "u_max": 40.029243, + "u_std": 7.094913, + "fronteiras_picos": 3 + }, + "taxa_ativos_ema": 1.0 + }, + "tkm": { + "k": 32, + "qe": 58.265549, + "te": 0.851562, + "distorcao": 6600.30127, + "sigma0": 2.0, + "sigma_f": 0.4, + "alpha0": 0.5, + "alpha_ultimo": 0.49505, + "n_ativos": 32, + "entropia": 3.465736, + "rank_efetivo": 32.0, + "frac_vivas": 1.0, + "u_matrix_resumo": { + "u_media": 33.759853, + "u_max": 60.711811, + "u_std": 13.525013, + "fronteiras_picos": 9 + }, + "taxa_ativos_ema": 1.0 + }, + "rsom": { + "k": 32, + "qe": 53.492386, + "te": 0.824219, + "distorcao": 5104.700684, + "sigma0": 2.0, + "sigma_f": 0.4, + "alpha0": 0.5, + "alpha_ultimo": 0.49505, + "n_ativos": 32, + "entropia": 3.465736, + "rank_efetivo": 32.0, + "frac_vivas": 1.0, + "u_matrix_resumo": { + "u_media": 34.29977, + "u_max": 60.189041, + "u_std": 13.249257, + "fronteiras_picos": 6 + }, + "taxa_ativos_ema": 1.0 + }, + "cpn": { + "k": 24, + "qe": 38.578434, + "te": 0.195312, + "distorcao": 884.967712, + "sigma0": 2.5, + "sigma_f": 0.4, + "alpha0": 0.5, + "alpha_ultimo": 0.462963, + "n_ativos": 23, + "entropia": 2.909895, + "rank_efetivo": 18.3549, + "frac_vivas": 0.9583, + "u_matrix_resumo": { + "u_media": 9.52423, + "u_max": 16.741587, + "u_std": 4.716956, + "fronteiras_picos": 3 + }, + "taxa_ativos_ema": 1.0 + }, + "ssom": { + "k": 24, + "qe": 123.85688, + "te": 0.90625, + "distorcao": 5260.532715, + "sigma0": 2.0, + "sigma_f": 0.4, + "alpha0": 0.5, + "alpha_ultimo": 0.49505, + "n_ativos": 24, + "entropia": 3.178054, + "rank_efetivo": 24.0, + "frac_vivas": 1.0, + "u_matrix_resumo": { + "u_media": 33.295235, + "u_max": 59.584949, + "u_std": 15.406026, + "fronteiras_picos": 4 + }, + "taxa_ativos_ema": 1.0 + } + }, + "inferencia_agente": { + "latencia_s": 0.382, + "tokens": 24, + "taxa_repeticao": 0.2083, + "coerencia_bigramas": 0.0 + }, + "difusao": { + "modo": "real", + "prompt_enriquecido": "o pantanal ao entardecer, com detalhes nítidos, com composição harmônica, com iluminação suave", + "latencia_s": 2.42, + "gerou_pixels": true + }, + "agente_engenheiro": { + "revisoes_aprovadas": 0, + "revisoes_bloqueadas": 0, + "som_invariante_sem_orfaos": true, + "som_ativos": { + "ativos/som": 1.0, + "k/som": 24, + "ativos/gg": 1.0, + "k/gg": 4, + "ativos/gcs": 1.0, + "k/gcs": 3, + "ativos/gsom": 1.0, + "k/gsom": 4, + "ativos/hsom": 1.0, + "k/hsom": 12, + "ativos/tkm": 1.0, + "k/tkm": 32, + "ativos/rsom": 1.0, + "k/rsom": 32, + "ativos/cpn": 1.0, + "k/cpn": 24, + "ativos/ssom": 1.0, + "k/ssom": 24, + "A_global": 1.0 + } + } +} \ No newline at end of file diff --git a/avaliacao_v8.json b/avaliacao_v8.json new file mode 100644 index 0000000000000000000000000000000000000000..41c29963f1a297f8ea6bdd7086dbc4660e0a4a98 --- /dev/null +++ b/avaliacao_v8.json @@ -0,0 +1,947 @@ +{ + "estado": "fase-som", + "roteador_ssom": { + "amostras": 1536, + "pronto": true, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.0, + "conf_media": 0.3382, + "frac_rotas_ativas": 0.75, + "k": 24, + "taxa_ativos": 1.0 + }, + "perdas_tarefa": { + "lm": { + "treinado": 7.840750614802043, + "aleatorio": 9.75594711303711 + }, + "noticia": { + "treinado": 7.552767038345337, + "aleatorio": 9.748649597167969 + }, + "instrucao": { + "treinado": 6.964608271916707, + "aleatorio": 9.766283671061197 + }, + "pontuacao": { + "treinado": 7.724856456120809, + "aleatorio": 9.752796649932861 + }, + "imagem_caption": { + "treinado": 7.004336357116699, + "aleatorio": 9.723368008931478 + }, + "vqa": { + "treinado": 7.352211872736613, + "aleatorio": 9.732653617858887 + }, + "ocr": { + "treinado": 7.486716111501058, + "aleatorio": 9.772692362467447 + }, + "asr": { + "treinado": 7.620069583257039, + "aleatorio": 9.76025120417277 + }, + "tts": { + "treinado": 6.79421067237854, + "aleatorio": 9.778711160024008 + } + }, + "ppl_lm": 2542.1122649587833, + "ppl_lm_aleatorio": 17256.55090604361, + "moe_uso_por_tarefa": { + "lm": [ + { + "experts_top": [ + 0, + 1 + ], + "grupo_top": [ + 0, + 0 + ] + }, + { + "experts_top": [ + 1, + 3 + ], + "grupo_top": [ + "dec", + "dec" + ], + "agrupado": false + } + ], + "vqa": [ + { + "experts_top": [ + 0, + 4 + ], + "grupo_top": [ + 0, + 2 + ] + }, + { + "experts_top": [ + 1, + 3 + ], + "grupo_top": [ + "dec", + "dec" + ], + "agrupado": false + } + ], + "ocr": [ + { + "experts_top": [ + 0, + 4 + ], + "grupo_top": [ + 0, + 2 + ] + }, + { + "experts_top": [ + 1, + 3 + ], + "grupo_top": [ + "dec", + "dec" + ], + "agrupado": false + } + ], + "asr": [ + { + "experts_top": [ + 4, + 0 + ], + "grupo_top": [ + 2, + 0 + ] + }, + { + "experts_top": [ + 1, + 3 + ], + "grupo_top": [ + "dec", + "dec" + ], + "agrupado": false + } + ], + "tts": [ + { + "experts_top": [ + 0, + 4 + ], + "grupo_top": [ + 0, + 2 + ] + }, + { + "experts_top": [ + 1, + 3 + ], + "grupo_top": [ + "dec", + "dec" + ], + "agrupado": false + } + ] + }, + "mtp": { + "alphas": [ + 0.0005292753921821713, + 0.9994707703590393 + ], + "per_cabeca": [ + 8.05044937133789, + 8.269538879394531 + ], + "entropia": 0.004521933849900961, + "termos_ce": 0.0 + }, + "w8a8_delta": 0.0006594657897949219, + "som_treinado": { + "variante_ativa": "cpn", + "variantes": { + "som": { + "k": 24, + "taxa_ativos": 1.0, + "resemeados": 24, + "usos": 256, + "atingiu_alvo": true + }, + "gg": { + "k": 4, + "taxa_ativos": 1.0, + "resemeados": 4, + "usos": 256, + "atingiu_alvo": true + }, + "gcs": { + "k": 3, + "arestas": 3, + "eq_treino": 4104.4892578125, + "atingiu_alvo": true + }, + "gsom": { + "k": 15, + "gt": 1.05, + "erro_medio": 0.18949279189109802, + "atingiu_alvo": true + }, + "hsom": {}, + "tkm": { + "k": 32, + "taxa_ativos": 1.0, + "resemeados": 32, + "usos": 256, + "atingiu_alvo": true + }, + "rsom": { + "k": 32, + "taxa_ativos": 1.0, + "resemeados": 32, + "usos": 256, + "atingiu_alvo": true + }, + "cpn": {}, + "ssom": { + "erro": "IndexError: index 8 is out of bounds for dimension 0 with size 8" + } + }, + "roteador": { + "amostras": 1581, + "pronto": true, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.009923, + "conf_media": 0.4107, + "frac_rotas_ativas": 0.7188, + "k": 24, + "taxa_ativos": 1.0 + }, + "invariante_sem_orfaos": true, + "orfaos_por_variante": { + "som": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "gg": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "gcs": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "gsom": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "hsom": { + "orfaos": 0, + "relocados": 0 + }, + "tkm": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "rsom": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "cpn": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "ssom": { + "orfaos": 0, + "relocados": 23, + "taxa_ativos": 1.0 + } + } + }, + "som_eq_fresco": { + "som": { + "taxa_ativos": 1.0, + "eq": 0.22861014306545258 + }, + "gg": { + "taxa_ativos": 1.0, + "eq": 1.0804760456085205 + }, + "gcs": { + "taxa_ativos": 1.0, + "eq": 0.9599178433418274 + }, + "gsom": { + "taxa_ativos": 1.0, + "eq": 1.2962698936462402 + }, + "tkm": { + "taxa_ativos": 1.0, + "eq": 0.08437514305114746 + }, + "rsom": { + "taxa_ativos": 1.0, + "eq": 0.07955622673034668 + }, + "ssom": { + "taxa_ativos": 1.0, + "eq": 0.09257030487060547 + } + }, + "geracao": { + "sem_punicao": " ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?", + "com_punicao": " ? ? ? ?\n ? ? ?feira ? ? ? ? ? ? ? ? ? ? O ? ? ? ? Uma ? corpo ? O ?" + }, + "microunidades": { + "lm": [ + { + "ramos": [ + "conv_dil", + "gru", + "mlp" + ], + "alpha_medio": [ + 0.6092, + 0.3907, + 0.0002 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "alpha_medio": [ + 0.1485, + 0.4984, + 0.0738, + 0.2794 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "alpha_medio": [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + "passos_rec": 2 + } + ], + "vqa": [ + { + "ramos": [ + "conv_dil", + "gru", + "mlp" + ], + "alpha_medio": [ + 1.0, + 0.0, + 0.0 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "alpha_medio": [ + 0.1592, + 0.4579, + 0.0731, + 0.3098 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "alpha_medio": [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + "passos_rec": 2 + } + ], + "asr": [ + { + "ramos": [ + "conv_dil", + "gru", + "mlp" + ], + "alpha_medio": [ + 0.6136, + 0.3863, + 0.0001 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "alpha_medio": [ + 0.1496, + 0.4945, + 0.074, + 0.2819 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "alpha_medio": [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + "passos_rec": 2 + } + ], + "instrucao": [ + { + "ramos": [ + "conv_dil", + "gru", + "mlp" + ], + "alpha_medio": [ + 0.839, + 0.161, + 0.0 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "alpha_medio": [ + 0.1472, + 0.5048, + 0.0738, + 0.2742 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "alpha_medio": [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + "passos_rec": 2 + } + ] + }, + "treino_prs_v8": { + "trust": 0.3768, + "tau": 6.82558, + "clip": 5.0, + "boost": 1.0, + "streak": 0, + "ciclos_sgdr": 0, + "recompensa_acum": 9.712, + "beta_min_relativo": 2.0 + }, + "treino_confianca": { + "h_ema": 0.398207426071167, + "ece": 0.13666666666666666, + "posterior_media": 0.18478260869565216, + "bernstein": [ + 0.048734518728612175, + 0.32083069866269215 + ] + }, + "treino_memoria": { + "slots": 13, + "capacidade": 64, + "escritas": 13, + "rejeitadas_confianca": 10, + "hit_rate": 1.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "treino_crescimento": [], + "memoria_execucao": { + "slots": 32, + "capacidade": 32, + "escritas": 45, + "rejeitadas_confianca": 0, + "hit_rate": 1.0, + "comprimidos": 32, + "entropia_codebook": 3.4869675636291504, + "perda_vq": "tensor(1.2586)", + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375, + "escritas_aceitas": 45, + "consultas_com_acerto": 15, + "comprimidos_agora": 32 + }, + "curva_epocas": [ + { + "epoca": 0, + "perda_media": 7.563339779175908, + "ppl_lm": 3175.163243261297 + }, + { + "epoca": 1, + "perda_media": 8.527180158174955, + "ppl_lm": 3338.0887647367836 + }, + { + "epoca": 2, + "perda_media": 7.530729611714681, + "ppl_lm": 2609.7989711637842 + }, + { + "epoca": 3, + "perda_media": 7.470894780158996, + "ppl_lm": 2415.108953044255 + }, + { + "epoca": 4, + "perda_media": 7.043362287374643, + "ppl_lm": 2283.2964058650737 + }, + { + "epoca": 5, + "perda_media": 7.089543087537899, + "ppl_lm": 2672.6735231256753 + }, + { + "epoca": 6, + "perda_media": 9.40745317524877, + "ppl_lm": 4403.193619695888 + }, + { + "epoca": 7, + "perda_media": 7.231130578782824, + "ppl_lm": 2572.532733229914 + } + ], + "coerencia": { + "corpus_25pct": { + "n_registros": 887, + "repeticao_pct": 69.85, + "ancoragem_bigramas": 0.0, + "entropia_logits": 0.6818 + }, + "corpus_50pct": { + "n_registros": 1774, + "repeticao_pct": 72.06, + "ancoragem_bigramas": 0.0, + "entropia_logits": 0.6818 + }, + "corpus_100pct": { + "n_registros": 3548, + "repeticao_pct": 56.62, + "ancoragem_bigramas": 0.0071, + "entropia_logits": 0.6818 + }, + "amostras": [ + " ? ? ? ? Uma ?\n ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? corpo implant ? ? ? ? ? ? ? ? ? ? ? rotina", + " ? ? Ofeira\n ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?\n ? ? ? ? ?", + " ? ? ? ? O ? ? Ele ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? rotina imped ? ?\n ? ? ? ? ? ? ?", + " ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Uma ? ? rotinaesquel ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?" + ] + }, + "clip_score": { + "clip_real": -0.0921, + "clip_controle": -0.0923, + "margem": 0.0002, + "n": 32 + }, + "itm": { + "acuracia": 0.5, + "f1": 0.0, + "n": 16, + "tp": 0, + "fp": 0, + "fn": 8 + }, + "ppl_visual": { + "ppl_visual": 1.52, + "n": 192, + "codigos_distintos": 9 + }, + "ppl_multimodal": { + "ppl_mm": 1072.72, + "ce_mm": 6.978, + "n": 2733 + }, + "geracao_metricas": { + "medias": { + "bleu": 0.0, + "rouge_l": 0.0026, + "meteor": 0.0, + "cider": 0.0 + }, + "n": 12, + "amostras": [ + { + "hipotese": " ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?", + "ref": "Na foto podemos ver um homem centralizado na imagem, posicio", + "bleu": 0.0, + "rouge_l": 0.0, + "meteor": 0.0, + "cider": 0.0 + }, + { + "hipotese": " ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?", + "ref": "Na foto podemos ver duas pessoas sobre uma prancha de stand ", + "bleu": 0.0, + "rouge_l": 0.0, + "meteor": 0.0, + "cider": 0.0 + }, + { + "hipotese": " ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?", + "ref": "Na foto podemos ver cinco pessoas reunidas ao redor de uma c", + "bleu": 0.0, + "rouge_l": 0.0, + "meteor": 0.0, + "cider": 0.0 + }, + { + "hipotese": " ? ? ? ? ? rotina básicos ? ? ? ? ? ? ? Uma ? ? ? ? ?", + "ref": "Na imagem podemos ver um homem usando uma jaqueta jeans de m", + "bleu": 0.0, + "rouge_l": 0.009666, + "meteor": 0.0, + "cider": 0.0 + } + ] + }, + "benchmarks": { + "mmmu": { + "benchmark": "MMMU-proxy-PT", + "n": 30, + "acuracia": 0.0, + "acertos": 0, + "distribuicao": { + "∅": 30 + }, + "acaso": 0.25, + "rotulo": "proxy em escala reduzida" + }, + "mme": { + "benchmark": "MME-proxy-PT", + "n": 15, + "acc": 0.0, + "acc_mais": 0.0, + "score_mme": 0.0, + "rotulo": "proxy em escala reduzida" + }, + "mathvista": { + "benchmark": "MathVista-proxy-PT", + "n": 2, + "acuracia": 0.0, + "acertos": 0, + "rotulo": "proxy em escala reduzida" + } + }, + "som_expandido": { + "som": { + "k": 24, + "qe": 0.728719, + "te": 0.671875, + "distorcao": 74.685822, + "sigma0": 3.0, + "sigma_f": 0.4, + "alpha0": 0.5, + "alpha_ultimo": 0.49505, + "n_ativos": 24, + "entropia": 3.178054, + "rank_efetivo": 24.0, + "frac_vivas": 1.0, + "u_matrix_resumo": { + "u_media": 2.167898, + "u_max": 6.077848, + "u_std": 1.605556, + "fronteiras_picos": 2 + }, + "taxa_ativos_ema": 1.0 + }, + "gg": { + "k": 4, + "qe": 9.097231, + "te": 0.878906, + "distorcao": 34.777397, + "sigma0": 3.0, + "sigma_f": 0.4, + "alpha0": 0.5, + "alpha_ultimo": 0.49505, + "n_ativos": 4, + "entropia": 1.386294, + "rank_efetivo": 4.0, + "frac_vivas": 1.0, + "u_matrix_resumo": { + "u_media": 2.047124, + "u_max": 2.419761, + "u_std": 0.425792, + "fronteiras_picos": 1 + }, + "taxa_ativos_ema": 1.0 + }, + "gcs": { + "k": 3, + "qe": 1.699399, + "te": 1.0, + "distorcao": 28.847206, + "sigma0": 0.0, + "sigma_f": 0.0, + "alpha0": 0.0, + "alpha_ultimo": 0.0, + "n_ativos": 3, + "entropia": 1.037116, + "rank_efetivo": 2.8211, + "frac_vivas": 1.0, + "u_matrix_resumo": { + "u_media": 0.0, + "u_max": 0.0, + "u_std": 0.0, + "fronteiras_picos": 0 + }, + "taxa_ativos_ema": null + }, + "gsom": { + "k": 4, + "qe": 10.148916, + "te": 0.859375, + "distorcao": 36.684849, + "sigma0": 3.0, + "sigma_f": 0.4, + "alpha0": 0.5, + "alpha_ultimo": 0.49505, + "n_ativos": 4, + "entropia": 1.386294, + "rank_efetivo": 4.0, + "frac_vivas": 1.0, + "u_matrix_resumo": { + "u_media": 1.536023, + "u_max": 2.269848, + "u_std": 0.599166, + "fronteiras_picos": 1 + }, + "taxa_ativos_ema": 1.0 + }, + "hsom": { + "k": 12, + "qe": 0.678309, + "te": 0.820312, + "distorcao": 82.439156, + "sigma0": 2.5, + "sigma_f": 0.4, + "alpha0": 0.5, + "alpha_ultimo": 0.49505, + "n_ativos": 12, + "entropia": 2.484907, + "rank_efetivo": 12.0, + "frac_vivas": 1.0, + "u_matrix_resumo": { + "u_media": 3.804719, + "u_max": 8.736095, + "u_std": 2.721405, + "fronteiras_picos": 2 + }, + "taxa_ativos_ema": 1.0 + }, + "tkm": { + "k": 32, + "qe": 0.216429, + "te": 0.851562, + "distorcao": 106.654846, + "sigma0": 2.0, + "sigma_f": 0.4, + "alpha0": 0.5, + "alpha_ultimo": 0.49505, + "n_ativos": 32, + "entropia": 3.465736, + "rank_efetivo": 32.0, + "frac_vivas": 1.0, + "u_matrix_resumo": { + "u_media": 3.442991, + "u_max": 11.211287, + "u_std": 2.545307, + "fronteiras_picos": 5 + }, + "taxa_ativos_ema": 1.0 + }, + "rsom": { + "k": 32, + "qe": 1.095875, + "te": 0.917969, + "distorcao": 70.848114, + "sigma0": 2.0, + "sigma_f": 0.4, + "alpha0": 0.5, + "alpha_ultimo": 0.49505, + "n_ativos": 32, + "entropia": 3.465736, + "rank_efetivo": 32.0, + "frac_vivas": 1.0, + "u_matrix_resumo": { + "u_media": 2.278218, + "u_max": 6.905422, + "u_std": 1.859771, + "fronteiras_picos": 7 + }, + "taxa_ativos_ema": 1.0 + }, + "cpn": { + "k": 24, + "qe": 0.182707, + "te": 0.3125, + "distorcao": 12.300114, + "sigma0": 2.5, + "sigma_f": 0.4, + "alpha0": 0.5, + "alpha_ultimo": 0.462963, + "n_ativos": 24, + "entropia": 2.561003, + "rank_efetivo": 12.9488, + "frac_vivas": 1.0, + "u_matrix_resumo": { + "u_media": 1.221734, + "u_max": 3.020689, + "u_std": 0.860318, + "fronteiras_picos": 2 + }, + "taxa_ativos_ema": 1.0 + }, + "ssom": { + "k": 24, + "qe": 0.824301, + "te": 0.945312, + "distorcao": 84.187256, + "sigma0": 2.0, + "sigma_f": 0.4, + "alpha0": 0.5, + "alpha_ultimo": 0.49505, + "n_ativos": 24, + "entropia": 3.178054, + "rank_efetivo": 24.0, + "frac_vivas": 1.0, + "u_matrix_resumo": { + "u_media": 1.757095, + "u_max": 8.05575, + "u_std": 1.751349, + "fronteiras_picos": 3 + }, + "taxa_ativos_ema": 1.0 + } + }, + "inferencia_agente": { + "latencia_s": 0.414, + "tokens": 24, + "taxa_repeticao": 0.9167, + "coerencia_bigramas": 0.0 + }, + "difusao": { + "modo": "planejado", + "prompt_enriquecido": "o pantanal ao entardecer, com detalhes nítidos, com composição harmônica, com iluminação suave", + "latencia_s": 0.0, + "gerou_pixels": false + }, + "agente_engenheiro": { + "revisoes_aprovadas": 0, + "revisoes_bloqueadas": 0, + "som_invariante_sem_orfaos": true, + "som_ativos": { + "ativos/som": 1.0, + "k/som": 24, + "ativos/gg": 1.0, + "k/gg": 4, + "ativos/gcs": 1.0, + "k/gcs": 3, + "ativos/gsom": 1.0, + "k/gsom": 4, + "ativos/hsom": 1.0, + "k/hsom": 12, + "ativos/tkm": 1.0, + "k/tkm": 32, + "ativos/rsom": 1.0, + "k/rsom": 32, + "ativos/cpn": 1.0, + "k/cpn": 24, + "ativos/ssom": 1.0, + "k/ssom": 24, + "A_global": 1.0 + } + } +} \ No newline at end of file diff --git a/comparacao_treino_v7.json b/comparacao_treino_v7.json new file mode 100644 index 0000000000000000000000000000000000000000..ae281fb6e53327d9ace39163e1dcdf80e25a940f --- /dev/null +++ b/comparacao_treino_v7.json @@ -0,0 +1,153 @@ +{ + "versao_atual": "v7-retreino-observado-ram", + "versao_anterior": "v6-roteamento-ssom-rpp-metricas", + "test_khtst_v6": { + "ok": 48, + "falhou": 0 + }, + "ram": { + "amostras": 6059, + "rss_min_mb": 542.6, + "rss_med_mb": 2692.2, + "rss_max_mb": 3579.5, + "disponivel_min_mb": 138.6, + "duracao_s": 12836.7 + }, + "perdas_tarefa": { + "asr": { + "anterior": 1.890599290529887, + "atual": 2.1221489111582437 + }, + "imagem_caption": { + "anterior": 1.6015255053838093, + "atual": 2.127371827761332 + }, + "instrucao": { + "anterior": 2.210473636786143, + "atual": 2.035900592803955 + }, + "lm": { + "anterior": 2.840959151585897, + "atual": 2.8384029467900596 + }, + "noticia": { + "anterior": 2.5332184632619223, + "atual": 2.5535371700922647 + }, + "ocr": { + "anterior": 2.109198252360026, + "atual": 1.418007344007492 + }, + "pontuacao": { + "anterior": 2.8601644039154053, + "atual": 2.648297905921936 + }, + "tts": { + "anterior": 1.7698550621668498, + "atual": 1.675029953320821 + }, + "vqa": { + "anterior": 2.4512375791867576, + "atual": 2.177152613798777 + } + }, + "som_taxa_ativos": { + "cpn": { + "anterior": null, + "atual": null + }, + "gcs": { + "anterior": null, + "atual": null + }, + "gg": { + "anterior": 1.0, + "atual": 1.0 + }, + "gsom": { + "anterior": null, + "atual": null + }, + "hsom": { + "anterior": null, + "atual": null + }, + "rsom": { + "anterior": 1.0, + "atual": 1.0 + }, + "som": { + "anterior": 1.0, + "atual": 1.0 + }, + "ssom": { + "anterior": null, + "atual": null + }, + "tkm": { + "anterior": 1.0, + "atual": 1.0 + } + }, + "rpp": { + "anterior": { + "rho": 1.0, + "streak": 0, + "recompensas": 0, + "punicoes": 2, + "penalidades": 99, + "kappa_rotas_mortas": 0.01 + }, + "atual": { + "rho": 1.0, + "streak": 0, + "recompensas": 0, + "punicoes": 2, + "penalidades": 104, + "kappa_rotas_mortas": 0.01 + } + }, + "alinhamento": { + "anterior": { + "clip_real": 0.1104, + "clip_controle": 0.1169, + "ppl_mm": 5.59, + "ppl_visual": 1.06, + "itm_acc": 0.5 + }, + "atual": { + "clip_real": -0.1604, + "clip_controle": -0.1702, + "ppl_mm": 9.41, + "ppl_visual": 1.16, + "itm_acc": 0.5 + } + }, + "ppl_lm": { + "anterior": 17.132190026332925, + "atual": 17.088452564794597, + "delta_pct": -0.26 + }, + "perda_media_final": { + "anterior": 0.78661770003876, + "atual": 0.8830587758666997 + }, + "passos": { + "anterior": 3192, + "atual": 3192 + }, + "nao_regressao": { + "antes": { + "ppl_lm": 17.132190026332925, + "perda_media_final": 0.78661770003876, + "taxa_ativos_min": 1.0 + }, + "depois": { + "ppl_lm": 17.088452564794597, + "perda_media_final": 0.8830587758666997, + "taxa_ativos_min": 1.0 + }, + "regressoes": {}, + "aprovado": true + } +} \ No newline at end of file diff --git a/comparacao_treino_v8.json b/comparacao_treino_v8.json new file mode 100644 index 0000000000000000000000000000000000000000..7fe96b9a237ba1f044dd4c22d0098d730a9122c5 --- /dev/null +++ b/comparacao_treino_v8.json @@ -0,0 +1,143 @@ +{ + "versao_atual": "v8-khtst-melhorias-a-j", + "versao_anterior": "aurora-v7-baseline-publicado", + "test_khtst_v6": { + "ok": 48, + "falhou": 0 + }, + "ram": { + "amostras": 5919, + "rss_min_mb": 428.7, + "rss_med_mb": 2524.4, + "rss_max_mb": 3358.4, + "disponivel_min_mb": 435.4, + "duracao_s": 14340.3 + }, + "perdas_tarefa": { + "asr": { + "anterior": 2.1221489111582437, + "atual": 7.620069583257039 + }, + "imagem_caption": { + "anterior": 2.127371827761332, + "atual": 7.004336357116699 + }, + "instrucao": { + "anterior": 2.035900592803955, + "atual": 6.964608271916707 + }, + "lm": { + "anterior": 2.8384029467900596, + "atual": 7.840750614802043 + }, + "noticia": { + "anterior": 2.5535371700922647, + "atual": 7.552767038345337 + }, + "ocr": { + "anterior": 1.418007344007492, + "atual": 7.486716111501058 + }, + "pontuacao": { + "anterior": 2.648297905921936, + "atual": 7.724856456120809 + }, + "tts": { + "anterior": 1.675029953320821, + "atual": 6.79421067237854 + }, + "vqa": { + "anterior": 2.177152613798777, + "atual": 7.352211872736613 + } + }, + "som_taxa_ativos": { + "cpn": { + "anterior": null, + "atual": null + }, + "gcs": { + "anterior": null, + "atual": null + }, + "gg": { + "anterior": 1.0, + "atual": 1.0 + }, + "gsom": { + "anterior": null, + "atual": null + }, + "hsom": { + "anterior": null, + "atual": null + }, + "rsom": { + "anterior": 1.0, + "atual": 1.0 + }, + "som": { + "anterior": 1.0, + "atual": 1.0 + }, + "ssom": { + "anterior": null, + "atual": null + }, + "tkm": { + "anterior": 1.0, + "atual": 1.0 + } + }, + "rpp": { + "anterior": {}, + "atual": { + "rho": 1.0, + "streak": 0, + "recompensas": 2, + "punicoes": 1, + "penalidades": 90, + "kappa_rotas_mortas": 0.01 + } + }, + "alinhamento": { + "anterior": { + "clip_real": -0.1604, + "clip_controle": -0.1702, + "ppl_mm": 9.41, + "ppl_visual": 1.16, + "itm_acc": 0.5 + }, + "atual": { + "clip_real": -0.0921, + "clip_controle": -0.0923, + "ppl_mm": 1072.72, + "ppl_visual": 1.52, + "itm_acc": 0.5 + } + }, + "ppl_lm": { + "anterior": 17.088452564794597, + "atual": 2542.1122649587833, + "delta_pct": 14776.2 + }, + "nao_regressao": { + "antes": { + "ppl_lm": 17.088452564794597, + "taxa_ativos_min": 1.0 + }, + "depois": { + "ppl_lm": 2542.1122649587833, + "perda_media_final": 7.231130578782824, + "taxa_ativos_min": 1.0 + }, + "regressoes": { + "ppl_lm": { + "antes": 17.088452564794597, + "depois": 2542.1122649587833, + "queda_relativa": 147.762 + } + }, + "aprovado": false + } +} \ No newline at end of file diff --git a/configs/base.json b/configs/base.json new file mode 100644 index 0000000000000000000000000000000000000000..a8203c86fdf224ba836c35a4a41abc504df5982b --- /dev/null +++ b/configs/base.json @@ -0,0 +1,215 @@ +{ + "modelo": { + "vocab": 16384, + "d_modelo": 192, + "n_camadas": 3, + "n_cabecas": 4, + "d_ff": 512, + "comprimento_ctx": 192, + "janela_sliding": 128, + "kv_cache_max": 2048, + "n_experts_fusao": 4, + "moe": { + "ativo": true, + "n_camadas_moe": 2, + "n_grupos": 3, + "experts_por_grupo": 2, + "d_ff_expert": 160, + "top_k": 2, + "margem_foco": 1.0, + "n_tarefas": 9, + "vetorial": true + }, + "mtp": { + "ativo": true, + "k_cabecas": 2, + "beta_entropia": 0.05, + "k_adaptativo": true + }, + "roteador": { + "ativo": true, + "k": 24, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "conf_min": 0.25, + "min_amostras": 200 + }, + "microunidades": { + "d_gru": 96, + "d_ramo": 96, + "d_refino": 128, + "kernel1": 3, + "kernel2": 5, + "gamma_rec": 0.5, + "k_rec_max": 2, + "eps_stop": 0.01, + "tau0": 0.8, + "kappa_conf": 1.0, + "delta_explora": 0.3 + }, + "atencao_moe": { + "ativo": true, + "alpha0": 0.0 + }, + "usar_atencao_arvore": true, + "moe_enc_dec": { + "ativo": true, + "n_encoders": 2, + "n_decoders": 4, + "d_ff_expert": 160, + "top_k": 2, + "fora_de_ordem": true + }, + "autoajuste": { + "ativo": true, + "janela_lr": 200, + "lr_passo": 0.5, + "raio_min": 32, + "raio_max": 256, + "alpha_peso_arvore": 0.05, + "ppl_alvo": 17.0 + } + }, + "treino": { + "epocas": 8, + "lote": 8, + "lr_max": 0.003, + "lr_min": 0.0003, + "warmup": 60, + "clip_grad": 1.0, + "janela_estagnacao": 120, + "checkpoint_a_cada": 150, + "avaliar_a_cada": 100, + "qat_ultimos_passos": 80, + "fase_densa_ate_epoca": 5, + "pcgrad": true, + "lambda_moe_lb": 0.01, + "lambda_moe_ort": 0.01, + "lambda_mtp": 0.3, + "lambda_som": 0.05, + "mistura_tarefas": { + "instrucao": 0.22, + "lm": 0.14, + "noticia": 0.1, + "pontuacao": 0.1, + "imagem_caption": 0.1, + "vqa": 0.1, + "ocr": 0.07, + "asr": 0.09, + "tts": 0.08 + }, + "abmo": { + "ativo": true, + "margem": 0.25, + "cap_min": 0.05, + "cap_max": 500.0 + }, + "dpo": { + "ativo": true, + "passos": 100, + "beta": 0.1, + "lr": 5e-05, + "lr_beta": 0.001, + "kl_alvo": 0.5, + "lote": 4, + "beta_min": 0.05 + }, + "fase_som": { + "ativo": true, + "epocas": 2, + "alvo_ativos": 0.9, + "gamma_empate": 0.15, + "lambda_novidade": 0.25, + "dim_lote": 64 + }, + "prs_v8": { + "ativo": true, + "eta_up": 0.05, + "eta_down": 0.02, + "T_ciclo": 400, + "decay_piso": 1.0, + "percentil": 0.75, + "janela_clip": 50, + "coef_streak": 0.3, + "vel_alvo": 0.01, + "boost_max": 1.0 + }, + "confianca": { + "ativo": true, + "kappa_punicao": 0.3, + "kappa_premio": 0.1, + "janela_tau": 50, + "quantil_tau": 0.25, + "h_escrita_memoria": 0.25 + }, + "crescimento": { + "ativo": true, + "a_cada": 200, + "tau_expand": 0.1, + "tau_prune": 0.01, + "eps_gate": 0.05, + "janela_uso": 8, + "n_max_ramos": 5, + "n_sondas": 2 + }, + "rpp": { + "ativo": true, + "rho_max": 0.15, + "rho_step": 0.05, + "rho_decaimento": 0.5, + "janela": 30, + "kappa_rotas_mortas": 0.01, + "alvo_ativos": 0.9 + }, + "sgdr_t_mult": 2 + }, + "ciclo": { + "tau_escalada": 0.35, + "entropia_limite": 0.85, + "orcamento_passos": 6, + "cache_ferramentas_ttl": 600, + "cache_ferramentas_max": 64 + }, + "memoria_interna": { + "ativo": true, + "n_slots": 64, + "h_escrita": 0.25, + "idade_compressao_s": 900.0, + "n_codigos": 64, + "lambda_utilidade": 0.1, + "meia_vida_s": 1800.0, + "replay_a_cada": 40 + }, + "checkpoints": { + "dir_local": "/home/z/my-project/khtst/estados_local", + "pasta_publicacao": "estados/fase-v8" + }, + "som_atencao": { + "ativo": true, + "tau": 0.5, + "gamma_novidade": 0.15 + }, + "difusao": { + "ativo": true, + "repo_id": "hf-internal-testing/tiny-stable-diffusion-torch", + "altura": 128, + "largura": 128, + "passos_inferencia": 8, + "guia_escala": 7.5 + }, + "agente_engenheiro": { + "ativo": true, + "intervalo": 50, + "tolerancia_regressao": 0.05 + }, + "janela_1m": { + "ativo": true, + "n_max_tokens": 262144, + "l_fino": 4096, + "w_segmento": 256, + "s_passo": 128, + "k_recall": 4, + "viz_r": 2, + "m_max": 2017 + } +} \ No newline at end of file diff --git a/docs/matematica/00-fundamentos-e-notacao.md b/docs/matematica/00-fundamentos-e-notacao.md new file mode 100644 index 0000000000000000000000000000000000000000..d9b0cb4f591e981886290f3b65c2f574369da816 --- /dev/null +++ b/docs/matematica/00-fundamentos-e-notacao.md @@ -0,0 +1,87 @@ +# 00 — Fundamentos e Notação (KHTST) + +> **Regra do projeto (item 7 do escopo):** nenhum script é escrito antes da análise matemática +> e da demonstração das equações que ele implementará. Este documento fixa a notação comum +> a todos os demais. Os documentos são independentemente legíveis e cada teorema lista +> explicitamente as hipóteses usadas. + +## 1. Notação + +| Símbolo | Significado | +|---|---| +| $\mathcal{X}$ | espaço de entrada (texto, imagem, áudio, vídeo, tabular, documentos) | +| $x_t \in \mathcal{X}$ | amostra no instante discreto $t \in \mathbb{N}$ | +| $W = \{w_1,\dots,w_K\}$, $w_i \in \mathbb{R}^d$ | protótipos/pesos de unidades de um SOM | +| $c(x) = \arg\min_i \|x - w_i\|$ | unidade vencedora (Best Matching Unit, BMU) | +| $\alpha(t) \in (0,1)$ | taxa de aprendizado no tempo | +| $h_{ci}(t)$ | função de vizinhança, $0 \le h_{ci} \le 1$, máx. em $i=c$ | +| $\sigma(t)$ | raio efetivo da vizinhança gaussiana | +| $E_q$ | erro de quantização médio: $E_q = \mathbb{E}\,\min_i \|x - w_i\|^2$ | +| $\theta$ | parâmetros treináveis do modelo neural (transformers etc.) | +| $Q(\cdot), s$ | quantização W8A8 e fator de escala | +| $\varphi(\cdot)$ | feature map de atenção linear (kernel trick) | +| $\mathcal{L}(\theta)$ | perda escalar a minimizar | + +Convenções: vetores são colunas; $\|\cdot\|$ é a norma euclidiana; $\odot$ produto de Hadamard; +$\mathbb{E}$ denota esperança empírica sobre o minibatch quando o contexto é amostral. +Aproximações numéricas usam `float32`; provas são analíticas e independentes da precisão. + +## 2. Princípios de projeto derivados da matemática + +1. **Estabilidade antes de plasticidade.** Toda atualização de parâmetro tem a forma + $\theta_{t+1} = \theta_t + \Delta_t$ com $\|\Delta_t\|$ controlado (taxa decrescente ou clip). + Isso garante que o "punir/retreinar" (doc 09) não desestabiliza o modelo. +2. **Convergência tipo Robbins–Monro.** Toda taxa de aprendizado cumpre + $\sum_t \alpha_t = \infty$ e $\sum_t \alpha_t^2 < \infty$ dentro de cada ciclo de treino + (ex.: decaimento coseno por reinício, doc 09, Teorema 9.4). +3. **Erro limitado por construção.** Quantização (doc 08) e camadas ortogonais (doc 09) + têm cotas de erro/distorção provadas — o modelo nunca opera com erro não caracterizado. +4. **Crescimento só com evidência.** Variantes crescentes de SOM (docs 02–04) inserem + unidades apenas onde o erro local o justifica; a monotonicidade do refinamento de + Voronoi (Teorema 2.1 do doc 02) garante que crescer nunca aumenta $E_q$. +5. **Memória recursiva estável.** Contextos temporais (doc 05) são médias móveis + exponenciais com fator $(1-\alpha)\in(0,1)$ — garantia de estabilidade BIBO. + +## 3. Teorema 0.1 (contração da atualização de Kohonen) + +**Hipóteses.** $0 < \alpha(t) \le 1$; $0 \le h_{ci}(t) \le 1$ para todo $c,i$. + +**Afirmação.** A atualização +$$w_i(t+1) = w_i(t) + \alpha(t)\,h_{c i}(t)\,\big(x(t) - w_i(t)\big)$$ +é uma contração em relação ao alvo $x$: vale +$$\|w_i(t+1) - x\| = \big(1 - \alpha(t) h_{ci}(t)\big)\,\|w_i(t) - x\| \;\le\; \|w_i(t) - x\|.$$ + +**Demonstração.** Substituindo, +$w_i(t+1) - x = w_i(t) - x + \alpha h_{ci}(x - w_i(t)) = \big(1 - \alpha h_{ci}\big)\big(w_i(t)-x\big)$. +Tomando normas, $\|w_i(t+1)-x\| = |1-\alpha h_{ci}|\,\|w_i(t)-x\|$. Como +$\alpha h_{ci} \in (0,1]$, tem-se $1-\alpha h_{ci} \in [0,1)$ e o resultado segue. $\blacksquare$ + +**Corolário 0.1.1 (não-explosão).** Se $\|x\| \le M$ (dados normalizados), então +$\|w_i(t)\| \le \max(M, \|w_i(0)\|)$ para todo $t$ — os pesos nunca divergem, o que +fundamenta os guard-rails de telemetria (doc 09, §5). + +## 4. Teorema 0.2 (desigualdade de Bhattacharyya/erro de partição — usada nos SOMs crescentes) + +Seja $V(w)$ a célula de Voronoi do protótipo $w$ e $E_q(V,w)=\int_V \|x-w\|^2 p(x)\,dx$. +Se $V$ é particionada em $V_1\cup V_2$ com novos protótipos $w_1\in V_1$, $w_2\in V_2$, então +$$E_q(V_1\cup V_2,\; w_1,w_2) \;\le\; E_q(V,\; w)$$ +**Demonstração.** Para cada $x\in V_1$: $\|x-w_1\|^2 \le \|x-w\|^2$? Não em geral ponto a ponto — +mas o ótimo por região satisfaz $w_1^\star=\mathbb{E}[x\mid x\in V_1]$ (centroide), e o mínimo +de uma função convexa restrita a um subconjunto não pode ser maior que o mínimo no conjunto +todo: $E_q(V_1,w_1^\star) \le E_q(V_1, w) \le E_q(V, w)$, pois $w\notin V_1$ é um candidato +viável (pior) para $V_1$. Somando sobre $V_1, V_2$ obtém-se o resultado. $\blacksquare$ + +**Uso prático:** inserção de unidades em SOMs crescentes (GG/GCS/GSOM) nunca piora o erro +de quantização *global* quando os novos centros são inicializados perto do pai — é a base +da prova do doc 02. + +## 5. Referências primárias + +- Kohonen, T. *Self-Organizing Maps*, 3ª ed. Springer (2001) — Teorema 0.1, condições de convergência. +- Robbins & Monro (1951) — aproximação estocástica. +- Fritzke, B. (1995; 1994) — Growing Grid, Growing Cell Structures. +- Alahakoon et al. (2000) — GSOM; Voegtlin (2002) — RSOM; Hecht-Nielsen (1987) — CPN. +- Roy & Vetterli (2007) — effective rank; Swets/Wu — PSI de população. +- Vaswani et al. (2017); Katharopoulos et al. (2020) — atenção e atenção linear. +- Dettmers et al. (2022) — LLM.int8(); W8A8 conforme docs 08. +- Bengio et al. (2013) — straight-through estimator. diff --git a/docs/matematica/01-som-kohonen-clasico.md b/docs/matematica/01-som-kohonen-clasico.md new file mode 100644 index 0000000000000000000000000000000000000000..4f80b0c74eff5dcfc03bb19e9827e7d6f310e917 --- /dev/null +++ b/docs/matematica/01-som-kohonen-clasico.md @@ -0,0 +1,84 @@ +# 01 — SOM de Kohonen clássico: derivação, convergência e ordenamento + +**Objetivo do módulo `memoria/som_base.py`.** Minimizar o erro de quantização vetorial +*topologicamente ordenado*: aproximar $p(x)$ por um conjunto $W=\{w_i\}_{i=1}^K$ de protótipos +dispostos numa grade cuja vizinhança preserva a topologia dos dados. + +## 1. Derivação da função objetivo + +O SOM minimiza (Erwin–Obermayer–Schulten, 1992) +$$E(W) \;=\; \sum_{i=1}^{K}\int_{V_i} \sum_{j=1}^{K} h_{ij}\,\|x - w_j\|^2\, p(x)\,dx,$$ +onde $V_i$ é a célula de Voronoi de $w_i$. Fazendo gradiente em $w_j$ e usando +$\int_{V_i} p(x) dx$ como frequência de ativação $n_i$: +$$\frac{\partial E}{\partial w_j} \;=\; -2\sum_i h_{ij}\, n_i\,\big(\mathbb{E}[x \mid V_i] - w_j\big),$$ +logo o gradiente descendente estocástico sobre amostras produz exatamente a **regra de Kohonen**: +$$\boxed{\;w_j(t+1) = w_j(t) + \alpha(t)\, h_{c(t),j}(t)\,\big(x(t) - w_j(t)\big),\qquad +c(t)=\arg\min_i \|x(t)-w_i(t)\|\;} \tag{1.1}$$ +com vizinhança gaussiana +$$h_{cj}(t) = \exp\!\Big(-\frac{\|r_c - r_j\|^2}{2\sigma(t)^2}\Big), \qquad \sigma(t)=\sigma_0\Big(\frac{\sigma_f}{\sigma_0}\Big)^{t/T}. \tag{1.2}$$ + +## 2. Teorema 1.1 (estabilidade com alvos móveis — extensão do Teorema 0.1) + +**Hipóteses.** $\alpha h_{cj}\in[0,1)$; $\|x(t)\|\le M$. +**Afirmação.** $\|w_j(t+1)\| \le (1-\alpha h)\,\|w_j(t)\| + \alpha h\, M$, e por indução +$\limsup_t \|w_j(t)\| \le M$. +**Demonstração.** Da regra (1.1), $\|w_j(t+1)\| \le (1-\alpha h)\|w_j(t)\| + \alpha h \|x(t)\| +\le (1-\alpha h)\|w_j(t)\| + \alpha h M$. A recorrência linear com ponto fixo $M$ dá +$\|w_j(t)\| \le M + (1-\alpha h)^t(\|w_j(0)\|-M) \to M$ quando $\alpha h>0$ com frequência +positiva. $\blacksquare$ + +## 3. Teorema 1.2 (convergência — condições de Robbins–Monro) + +**Hipóteses.** +(R1) $\alpha(t) = \alpha_0 (t+1)^{-\gamma}$ com $\gamma\in(\tfrac12,1]$; +(R2) os dados são i.i.d. de $p$ com suporte compacto; +(R3) $\sigma(t)\to\sigma_f > 0$ (fase de convergência após ordenamento). + +**Afirmação.** $E\big[\|W(t+1)-W(t)\|^2\big] \to 0$ e $W(t)\to W^\star$ q.s., com $W^\star$ +ponto estacionário de $E(W)$. + +**Esboço de demonstração** (padrão de Kohonen 2001, cap. 3.5; Erwin et al. 1992). +Escreva a atualização como processo estocástico aproximator +$W(t+1)-W(t) = \alpha(t)\big[\,F(W(t)) + G(t)\,\big]$, com +$F(W) = \mathbb{E}_x\big[h_{c(x),\cdot}\,(x-W_\cdot)\big]$ o campo médio e $G(t)$ ruído centrado +condicional. As condições (R1) dão $\sum\alpha=\infty$, $\sum\alpha^2<\infty$ — exatamente as +condições de Robbins–Monro — e então, pela teoria clássica de aproximação estocástica, +$W(t)$ converge q.s. ao conjunto estacionário de $F$, desde que $G$ tenha momento finito +(garantido por R2) e $F$ seja Lipschitz (garantido pela suavidade de $h$ gaussiana e +compacidade do suporte). $\blacksquare$ + +**Consequência de engenharia (implementada em `treinador.py`):** a fase 1 (ordenamento) usa +$\sigma$ alto e $\alpha$ alto; a fase 2 (convergência) usa $\sigma=\sigma_f$ pequeno e +$\alpha$ decaindo como $(t+1)^{-\gamma}$ — nunca constante para sempre, evitando oscilação +estacionária (que a telemetria classificaria como *não-aprendizado*). + +## 4. Teorema 1.3 (ordenamento topológico 1-D — prova completa de Erwin et al.) + +**Hipóteses.** Grade 1-D com $K$ unidades, pesos escalares ordenáveis $w_1<\dotsw_{i+1}$ com $\mu_i<\mu_{i+1}$, onde +$\mu_i=\int_{V_i} x\,p(x)dx$ é o centroide da célula) é corrigida com probabilidade 1. + +**Demonstração.** Defina a troca de energia esperada por passo para um par invertido. +Para $K=2$ e vizinhança passo-1, o erro quadrático esperado +$E(w_1,w_2)=\int_{-\infty}^{m}\!(x-w_1)^2 p dx + \int_m^\infty\!(x-w_2)^2 p dx$ (com $m$ a fronteira +de Voronoi) tem única bacia de mínimo em $w_1=\mu_10$, o que diminui a distância entre $\mu$ e $w$ nos pares não-invertidos e corrige a +inversão mínima; removendo-se o par, repete-se. O processo termina pois $E(W)$ é função de +Lyapunov estritamente decrescente fora do conjunto estacionário. $\blacksquare$ + +*(No projeto, o ordenamento é verificado empiricamente pela telemetria: correlação de +Spearman entre posição na grade e 1ª componente principal das amostras, `indicadores.py`.)* + +## 5. Métricas derivadas + +1. **EQ (erro de quantização):** $\widehat{E_q} = \frac1n\sum_k \min_i\|x_k-w_i\|^2$ — decresce + monotonicamente por Teorema 0.2 quando unidades crescem. +2. **TE (erro topográfico):** fração de amostras cujas 2 BMUs não são vizinhas na grade — + mede a qualidade do mapeamento. +3. **Rank efetivo da matriz $U$ de ativações por unidade** (doc 09, §4) — mede diversidade + de uso das unidades; rank baixo = unidades mortas (dispara poda/retreino, item 6 do escopo). diff --git a/docs/matematica/02-growing-grid-e-gcs.md b/docs/matematica/02-growing-grid-e-gcs.md new file mode 100644 index 0000000000000000000000000000000000000000..4af12f1bb98d4918973029ae6130ac0a03982258 --- /dev/null +++ b/docs/matematica/02-growing-grid-e-gcs.md @@ -0,0 +1,76 @@ +# 02 — Growing Grid (GG) e Growing Cell Structures (GCS) + +**Módulos:** `memoria/gg.py`, `memoria/gcs.py`. Ideia de Fritzke (1995, 1994): em vez de +grade fixa, **inserir/remover unidades** onde o erro acumulado é maior, ajustando $K$ ao dado. + +## 1. GG — inserção por linhas/colunas (Fritzke, 1995) + +Estrutura: grade retangular $L_1\times L_2$. Cada unidade $i$ acumula erro +$e_i \leftarrow e_i + \|x - w_{c}\|^2$ por amostra (decaimento periódico $e_i \leftarrow (1-\beta)e_i$). +A cada $\lambda$ amostras, na unidade $q=\arg\max_i e_i$ insere-se uma **linha ou coluna** +(escolhida pela direção de maior dispersão de $q$ com seus vizinhos), distribui-se o erro de $q$ +entre os novos vizinhos e decai todos os erros. + +### Teorema 2.1 (monotonicidade do refinamento — fundamento da inserção) + +**Hipóteses.** Protótipo novo inicializado na média entre $q$ e seu vizinho mais distante $v$ +(perto da célula de maior erro); população fixa $p$. +**Afirmação.** $\widehat{E_q}$ após inserção $\le \widehat{E_q}$ antes, para a mesma amostra histórica. +**Demonstração.** A partição de Voronoi após inserção é um **refinamento** da anterior: cada +antiga célula é subdividida ou mantida. Para cada amostra $x$ que permanece na célula de $q$ +sem subdivisão, o erro é idêntico. Para $x$ que migra para o novo protótipo $w_{new}$: +$\|x - w_{new}\| \le \|x - w_q\|$ se, e somente se, $w_{new}$ está mais perto — verdade para a +sub-região do lado de $w_{new}$ (definição de fronteira de Voronoi). Logo o erro empírico +ponto a ponto não aumenta em nenhuma amostra, e o ótimo por região (Teorema 0.2) só o reduz. +Portanto $E_q$ é **monótona não-crescente** sob inserção. $\blacksquare$ + +*(Isso responde diretamente ao item 7 do escopo: "se um problema não tem solução imediata, +outra equação auxilia" — quando a grade fixa satura em $E_q$, a equação de refinamento +(Teorema 2.1) abre caminho em vez de estagnar.)* + +### Escolha da dimensão a crescer +Seja $v_1, v_2$ os dois vizinhos de $q$ mais distantes entre si. Cresce-se na direção +$v_1 \leftrightarrow v_2$ (Fritzke usa $L_1/L_2$ pela razão de distâncias médias): +direção $d^\star = \arg\max_{d\in\{1,2\}} \frac{\bar d_d(q)}{L_d}$, garantindo grade o mais +quadrada possível — minimiza $\max_i \|r_i - r_j\|$ médio, i.e. o raio da vizinhança. + +## 2. GCS — crescimento simplicial com poda por utilidade (Fritzke, 1994) + +Topologia: complexo simplicial 2-D (triângulos) ou 3-D (tetraedros); cada nova unidade +insere-se **no meio da aresta** $(q, f)$ com $f$ = vizinho mais distante de $q$. + +### Teorema 2.2 (conectividade preservada após inserção) + +**Hipóteses.** Rede inicial um triângulo (conectada). Inserção de $w_{new}$ no ponto médio da +aresta $(q,f)$, conectado a $q$, $f$ e a todos os vizinhos comuns de $q$ e $f$. +**Afirmação.** A rede permanece conexa e a vizinhança direta refina a anterior. +**Demonstração.** Remover a aresta $(q,f)$ e reconectar ambos a $w_{new}$ preserva caminhos: +todo caminho que usava $(q,f)$ usa $(q,w_{new})+(w_{new},f)$. Vizinhos comuns ganham aresta a +$w_{new}$ sem perder as antigas — o grau só aumenta localmente. Conexidade é invariante. $\blacksquare$ + +### 2.3 Distribuição de erro e remoção (pruning) +Inserção: $e_{new} = e_q/2$; $e_q \leftarrow e_q/2$; vizinhos decaem $e_i \leftarrow (1-\beta)e_i$. +**Utilidade** (Fritzke): $u_i = e_i - e_{i^\dagger}$ não funciona para poda direta; usa-se +$u_i = \frac{\text{número de usos como 2ª BMU}}{\text{usos como 1ª BMU}}$ ponderado, ou a versão +clássica: $u_i$ = erro que a rede teria se $w_i$ fosse removido e suas amostras absorvidas pelo +vizinho mais útil. Remove-se a unidade com menor $u_i$ se $K>K_{max}/2$ e $u_i < u_{max}/k_{prune}$. + +**Teorema 2.3 (podas não aumentam $E_q$ além de cota local).** Remover $w_i$ realoca seus +pontos ao vizinho mais próximo $w_j$: $\|x-w_j\| \le \|x-w_i\| + \|w_i - w_j\|$ para esses pontos, +logo $\Delta E_q \le \int_{V_i}\big(\|x-w_j\|^2-\|x-w_i\|^2\big)p\,dx \le 2\,\|w_i-w_j\|\,\|w_i\|_V + \|w_i-w_j\|^2$, +com $\|w_i-w_j\|$ pequeno por construção da vizinhança — poda é *segura* quando a unidade é +redundante ($w_j$ quase coincide com $w_i$), que é exatamente o caso detectado por $u_i$ baixo. $\blacksquare$ + +## 3. Parâmetros implementados (e sua justificativa matemática) + +| Parâmetro | Valor padrão | Justificativa | +|---|---|---| +| $\lambda$ | 600 | período entre inserções: compromisso entre adaptação e crescimento | +| $\beta$ | 0.9995 | decaimento do erro acumulado (meia-vida $\approx 1385$ amostras) | +| $\alpha_{burnin}$ | decaimento (1.1) | convergência Robbins–Monro (Teorema 1.2) | +| $K_{max}$ | orçamento de RAM | acoplado ao requisito de otimização de memória | + +## 4. Critério de parada de crescimento +Crescer enquanto $\max_i e_i > \tau_{grow}\cdot \overline{E_q}$ e $K GT$ na BMU $c$ **e** $c$ é uma unidade +de fronteira, cresce uma nova unidade na direção livre com +$w_{new} = w_c + (w_c - w_{vizinho})$ (espelhamento para fora). + +### Teorema 3.1 (derivação de $GT$ a partir da distância média entre vizinhos) + +**Afirmação.** $GT$ equivale à distância quadrática que a rede considera "resolvida". +**Demonstração.** Sejam $x,x'$ amostras vizinhas em uma mesma região já bem quantizada, com +distância média intra-região $\delta$. A rede deve crescer somente quando o erro acumulado +local supera a escala natural dos dados. Sob normalização para $[0,1]^D$, a distância média +entre pares aleatórios é $\mathbb{E}\|x-x'\|^2 = 2D\cdot\mathrm{Var}$ com $\mathrm{Var}=1/12$ +(uniforme), dando $\mathbb{E}\|x-x'\|^2 = D/6$. O GSOM padroniza o limiar como múltiplo +negativo-logarítmico do $SF$ para obter monotonicidade estrita e invariância de escala: +$GT(SF)=-D\ln(SF)$ é **decrescente em $SF$** ($\partial GT/\partial SF = -D/SF < 0$), logo: +$$SF_1 < SF_2 \;\Rightarrow\; GT_1 > GT_2 \;\Rightarrow\; \text{crescimento mais raro} \;\Rightarrow\; \text{mapa menor.} \tag{3.2}$$ +A forma logarítmica garante que duplicações sucessivas de resolução correspondam a incrementos +aditivos constantes em $\ln(1/SF)$ — granularidade geométrica, não linear. $\blacksquare$ + +**Corolário 3.1.1 (orçamento de memória).** Como $K(SF)$ cresce aproximadamente como +$\mathcal{O}\big(SF^{-c}\big)$ para constante $c\in(0,1)$ dependente de $p$, o $SF$ dá controle +direto e **pré-computável** do uso de RAM — requisito do item 1 do escopo. + +## 2. Fases e suavização + +1. **Inicialização:** 4 unidades $(0,0),(0,1),(1,0),(1,1)$ em grade mínima. +2. **Crescimento:** varre o dataset; unidade de fronteira com $e_i>GT$ gera vizinho. +3. **Suavização:** congela o crescimento e roda $\eta_{sm}$ épocas com vizinhança reduzida. + +**Teorema 3.2 (finitude).** Com $SF>0$ fixo e dados de suporte compacto, o mapa atinge tamanho +finito quase certamente. +**Demonstração.** Cada crescimento exige $e_i>GT>0$. Após suficientes apresentações, o erro +médio por unidade tende ao mínimo local $\bar\epsilon(\sigma_f)$ (Teorema 1.2). Se +$\bar\epsilon(\sigma_f) \le GT$, apenas um número finito de unidades pode ultrapassar $GT$ +(massa de probabilidade finita acima do limiar); cada unidade nova tem vizinhança própria que +reduz localmente o erro (Teorema 0.2/2.1), então a probabilidade de novos disparos decresce +estritamente e a soma de disparos é finita q.s. (argumento de supermartingale em +$\sum_i \mathbb{P}(e_i>GT \text{ após adaptação})$). $\blacksquare$ + +## 3. Comparação com GG/GCS (por que os três existem no KHTST) + +| | GG | GCS | GSOM | +|---|---|---|---| +| Topologia | retangular | simplicial | retangular livre | +| Controle de tamanho | $K_{max}$ | $K_{max}$ + poda | $SF$ contínuo | +| Crescimento | linhas/colunas | arestas | unidades de fronteira | +| Indicador de parada | cotovelo de $E_q$ | utilidade baixa | Teorema 3.2 (finitude) | + +O **orquestrador SOM-of-SOMs** (`memoria/orquestrador.py`) escolhe a variante por perfil: +dados densos e estáveis → GG; necessidade de detectar redundância → GCS (poda); +orçamento de RAM apertado e granularidade desejada → GSOM. diff --git a/docs/matematica/04-hierarchical-som.md b/docs/matematica/04-hierarchical-som.md new file mode 100644 index 0000000000000000000000000000000000000000..a06d7edab20885a8fb40c10e86c847344c0232f3 --- /dev/null +++ b/docs/matematica/04-hierarchical-som.md @@ -0,0 +1,50 @@ +# 04 — Hierarchical SOM (H-SOM): erro decomposto por nível + +**Módulo:** `memoria/hsom.py`. Ideia: empilhar SOMs em níveis; o nível $1$ quantiza grosso, +cada unidade $i$ do nível 1 governa um mapa filho $\mathcal{M}_i$ do nível 2 que quantiza o +resíduo (ou a sub-população) daquela região. + +## 1. Decomposição do erro + +Seja $x$ atribuído ao protótipo $w^{(1)}_i$ no nível 1 e, dentro do mapa filho $\mathcal{M}_i$, +ao protótipo $w^{(2)}_{ij}$. O erro total de reconstrução de dois níveis é +$$E^{(2)}(x) = \|x - w^{(1)}_i\|^2 + \|x - w^{(2)}_{ij}\|^2 - 2\,(x-w^{(1)}_i)^\top (x-w^{(2)}_{ij}) \;\le\; 2\big(\|x-w^{(1)}_i\|^2 + \|x-w^{(2)}_{ij}\|^2\big), \tag{4.1}$$ +pela desigualdade $2ab \le a^2+b^2$. A composição hierárquica implementa +$$w^{(2)}_{ij} \approx w^{(1)}_i + \text{correção local},$$ +e o erro **residual** $\|x - w^{(2)}_{ij}\|$ satisfaz: + +**Teorema 4.1 (hierarquia reduz o raio efetivo).** Se nível 1 tem $K_1$ unidades cobrindo o +suporte $\Omega$ com diâmetro $\Delta$, cada célula de Voronoi do nível 1 tem diâmetro +$\le \Delta/\sqrt{K_1}$ em média (partição uniforme), e o nível 2 opera apenas dentro dessas +células — a distância que os protótipos filhos precisam alcançar é menor que a global, logo +com o mesmo orçamento total $K_1 + K_2$ unidades, $E_q$ hierárquico $\le E_q$ de um mapa plano +com $K_1$ unidades quando as distribuições locais são multimodais. +**Demonstração (esboço).** Para população local $p_i$ multimodal dentro da célula $V_i$, um +mapa plano com $K_1$ centros comete erro $\approx$ variância intra-célula total. A hierarquia +gasta $K_2$ centros para capturar os modos locais; pelo Teorema 0.2 (refinamento), a soma dos +erros das partições finas é $\le$ erro da partição grossa. Somando sobre $i$: $E^{(2)}\le E^{(1)}$ +quando $K_2$ é alocado às células de maior massa de erro (greedy pelo erro acumulado). $\blacksquare$ + +## 2. Treinamento em dois estágios (evita beco sem saída — item 7) + +1. Estágio A: treinar nível 1 até $\sigma$ pequeno (Teorema 1.2). +2. Estágio B: **congelar** nível 1 e treinar cada filho com as amostras que caem na sua célula + (rotação por unidade — memória amigável: só um filho em RAM por vez). +3. Estágio C (opcional): descongelar nível 1 com $\alpha$ pequeno ($10^{-3}$) para ajuste fino. + +**Teorema 4.2 (estágio B não degrada o nível 1).** Congelar $W^{(1)}$ torna a atribuição de +célula invariante; o erro total é soma de termos independentes, e minimizar cada termo filho +minimiza a soma — não há termo cruzado ativo. $\blacksquare$ + +## 3. Custo computacional + +Busca BMU hierárquica: $O(K_1) + O(K_2)$ em vez de $O(K_1 K_2)$ de um mapa plano equivalente — +a busca em dois níveis é um índice (quantização produto por partição), com +$K_{plano}=K_1\cdot K_2/K_1=K_2$ unidades por célula média; ganho prático de RAM: +só o filho ativo fica residente. + +## 4. Uso no KHTST + +H-SOM implementa a **memória episódica multinível** do raciocínio cíclico: nível 1 = "tipo de +situação", nível 2 = "detalhe da situação". O plano do ciclo PDCA consulta o nível 1 +(barato, sempre em RAM) e só carrega o filho relevante sob demanda (item 1: otimização de RAM). diff --git a/docs/matematica/05-tkm-rsom.md b/docs/matematica/05-tkm-rsom.md new file mode 100644 index 0000000000000000000000000000000000000000..06a8a02a658c54399fa3f836c046ff827ac8c08a --- /dev/null +++ b/docs/matematica/05-tkm-rsom.md @@ -0,0 +1,59 @@ +# 05 — Variantes temporais: TKM e RSOM (Kohonen com memória) + +**Módulos:** `memoria/tkm.py`, `memoria/rsom.py`. Referências: Kangas (1990) TKM; +Voegtlin (2002) RSOM. + +## 1. Temporal Kohonen Map (TKM) + +O TKM define o vencedor não pela amostra corrente, mas pelo **erro acumulado na janela temporal**: +$$D_i(t) \;=\; \sum_{s=0}^{W-1} \lambda^s\,\|x(t-s) - w_i\|^2, \qquad \lambda\in(0,1), \tag{5.1}$$ +$c(t) = \arg\min_i D_i(t)$; a atualização usa a regra de Kohonen na amostra corrente. + +**Teorema 5.1 (memória efetiva finita).** A contribuição da amostra $x(t-s)$ em $D_i(t)$ é +$\lambda^s\|x(t-s)-w_i\|^2$; como $\sum_{s\ge0}\lambda^s = \frac{1}{1-\lambda}$, a soma das +contribuições além de $S_\epsilon$ é $\frac{\lambda^{S_\epsilon+1}}{1-\lambda}$. Para capturar +$(1-\epsilon)$ da massa de memória: $S_\epsilon = \frac{\ln(\epsilon(1-\lambda))}{\ln\lambda} - 1$. +**Demonstração.** Série geométrica direta + desigualdade do resto. $\blacksquare$ +**Uso:** a janela efetiva $S_\epsilon$ é logada pela telemetria; $\lambda$ padrão $=0.5$ +(janela média $=1/(1-\lambda)=2$ amostras; configurável). + +## 2. Recurrent SOM (RSOM) — diferenças acumuladas + +O RSOM acumula o **resíduo** em vez da distância: +$$\boxed{\;c_i(t) = (1-\alpha)\,c_i(t-1) + \alpha\,\big(x(t) - w_i(t)\big)\;} \tag{5.2}$$ +com vencedor $c(t) = \arg\min_i \|c_i(t)\|$ e atualização de Kohonen aplicada ao longo da +sequência. Expandindo (5.2): +$$c_i(t) = \alpha\sum_{s=0}^{t-1}(1-\alpha)^s\,\big(x(t-s)-w_i(t-s)\big) + (1-\alpha)^t c_i(0). \tag{5.3}$$ + +**Teorema 5.2 (c é média móvel exponencial do resíduo).** (5.3) segue por indução: +$c_i(t+1) = (1-\alpha)\big[\alpha\sum_{s=0}^{t-1}(1-\alpha)^s r_i(t-s)\big] + \alpha r_i(t) += \alpha\sum_{s=0}^{t}(1-\alpha)^s r_i(t+1-(s+1))$. $\blacksquare$ + +**Teorema 5.3 (estabilidade BIBO).** Se $\|x-w_i\|\le B$ e $c_i(0)$ finito, então +$\|c_i(t)\| \le \alpha B \frac{1-(1-\alpha)^t}{\alpha} + (1-\alpha)^t\|c_i(0)\| \le B + o(1)$ — +o contexto é limitado independentemente do comprimento da sequência. +**Demonstração.** Norma triangular em (5.3) + soma geométrica. $\blacksquare$ + +**Teorema 5.4 (filtro passa-baixa).** No domínio $z$, (5.2) é +$C_i(z) = \frac{\alpha}{1-(1-\alpha)z^{-1}} R_i(z)$ — filtro de 1ª ordem com polo em $1-\alpha\in(0,1)$ +(estável, já que o polo está dentro do círculo unitário). Frequência de corte +$\omega_c \approx \alpha$ rad/amostra: $\alpha$ pequeno ⇒ memória longa e suave. $\blacksquare$ + +## 3. Diferença prática TKM × RSOM (e por que ambas) + +| | TKM | RSOM | +|---|---|---| +| Acumula | distâncias (escalares positivos) | resíduos vetoriais (cancela componentes) | +| Vencedor | erro ponderado no tempo | norma do contexto | +| Detecta | duração de padrões (prolongamento) | **tendência** (direção média do resíduo) | +| RAM | $O(K)$ | $O(Kd)$ | + +No orquestrador, TKM roteia tarefas com "eventos longos" (áudio/vídeo) e RSOM tarefas com +transições de estado (diálogos, raciocínio multi-passo). + +## 4. Inicialização e higiene numérica + +$c_i(0)=0$ (equivale a assumir resíduo nulo — o vencedor inicial coincide com o BMU estático); +a cota $\|c_i\|\le B$ é checada a cada lote por asserção local (Teorema 5.3) e tratada +como **bug** (não como dados) — a revisão de contratos antes de cada modificação é +responsabilidade do **Agente Engenheiro** (ver README). diff --git a/docs/matematica/06-cpn-ssom.md b/docs/matematica/06-cpn-ssom.md new file mode 100644 index 0000000000000000000000000000000000000000..30477443039987572b72abb316f2af6e333ae8a3 --- /dev/null +++ b/docs/matematica/06-cpn-ssom.md @@ -0,0 +1,58 @@ +# 06 — Counterpropagation Network (CPN) e Supervised SOM (S-SOM) + +## 1. CPN (Hecht-Nielsen, 1987) + +Arquitetura em duas camadas: **Kohonen** (competitiva, WTA) + **Grossberg** (outstar, LMS). +Dado par $(x, y)$: camada 1 escolhe $c=\arg\min_i\|x-w_i\|$; camada 2 aprende +$$v_j \leftarrow v_j + \eta_g\,(y - \hat y)_j\,\mathbb{1}[i=c], \qquad \hat y = v_c. \tag{6.1}$$ + +### Teorema 6.1 (convergência por célula — regressão por partes) + +**Hipóteses.** $W$ congelado após sua fase (alternância de fases); $\eta_g = \eta_0/t$ com +$\eta_0>0$; pares i.i.d. +**Afirmação.** Para cada célula $c$, $v_c(t) \to \bar y_c := \mathbb{E}[y \mid c(x)=c]$ +(média condicional), que é o minimizador de $\mathbb{E}[\|y-v\|^2 \mid c(x)=c]$. +**Demonstração.** Com $W$ congelado, a atribuição $c(x)$ é uma função determinística de $x$. +Restringe-se (6.1) às amostras de $c$: $v \leftarrow v + \eta_t (y_t - v)$, que é o SGD de +$J(v)=\frac12\mathbb{E}[\|y-v\|^2 \mid c]$ com gradiente $\mathbb{E}[v-y\mid c]$. $J$ é +fortemente convexa com mínimo em $\bar y_c$; por Robbins–Monro ($\sum\eta=\infty$, +$\sum\eta^2<\infty$), $v\to\bar y_c$ q.s. A saída global é, portanto, a **regressão por +partes** (piecewise regression) sobre a partição de Voronoi de $W$. $\blacksquare$ + +**Uso no KHTST:** a CPN é o "conector" SOM→saída simbólica: quantiza o estado latente e +produz, por região, a resposta média — base do **decodificador de roteamento** do orquestrador +e do preditor de ferramenta no ciclo PDCA. + +### 6.2 Duas fases, duas taxas +Fase 1 (Kohonen): $\alpha_K$ decaindo (Teorema 1.2). Fase 2 (Grossberg): $\eta_g$ decaindo. +Alternância evita alvo móvel (Teorema 6.1 exige $W$ congelado). + +## 2. S-SOM (Supervised SOM) + +Objetivo: mapa auto-organizável **com rótulos**, misturando quantização e classificação. +Perda combinada: +$$J(W, V) = \underbrace{\mathbb{E}\big[\textstyle\sum_j h_{cj}\|x-w_j\|^2\big]}_{\text{quantização (não-supervisionada)}} \;+\; \lambda\, \underbrace{\mathrm{CE}\big(y,\; \mathrm{softmax}(V W_a)\big)}_{\text{supervisão}}, \tag{6.2}$$ +onde $a(x)=\arg\min_i\|x-w_i\|$ e $V W_a$ lê o vetor de pontuação da unidade vencedora. + +**Teorema 6.2 (equilíbrio ótimo de $\lambda$ por normalização de escala).** Se +$\|x\|=1$ (entradas normalizadas) e $\|v\|\le v_{max}$, os gradientes dos dois termos são da +ordem de $\sigma_x$ e $\sigma_{CE}$; escolher $\lambda = \sigma_x/\sigma_{CE}$ iguala as +magnitudes de gradiente — implementado com $\sigma$ medidos por EMA na telemetria +(autoajuste com taxa de Robbins–Monro $\beta_t$): $\lambda_{t+1} = \lambda_t(1+\beta_t(\sigma_x/\sigma_{CE} - \lambda_t)/\lambda_t)$. +**Demonstração.** Gradiente do termo 1 w.r.t. $w_j$: $2h_{cj}(x-w_j)$ com norma esperada +$\approx 2\sigma_x$ (Teorema 0.1). Gradiente do termo CE via softmax: norma esperada +$\propto \sigma_{CE}$. Igualar esperanças normaliza as taxas efetivas — condição de +estabilidade de passo único (Teorema 0.1) para ambos. $\blacksquare$ + +**Teorema 6.3 (rótulos não destroem a topologia).** Para $\lambda \le \lambda_{max}$ finito, o +termo de quantização domina em coeficiente de Lipschitz nos protótipos na medida em que +$h$ tem suporte local; a ordenação de Teorema 1.3 sobrevive com distorção limitada por +$\lambda/\alpha$ (distância máxima que o gradiente CE move um peso por passo). +**Consequência de engenharia:** limitamos $\lambda\, v_{max} \le 0.1\,\alpha$ na implementação +(guarda em `ssom.py`), garantindo a cota. + +## 3. Papel das duas no pipeline + +- **CPN** → roteamento estado→ação (qual módulo/ferramenta executa o próximo passo do PDCA). +- **S-SOM** → classificador de intenção com memória organizada (intenção→rota no arbiter); + também fornece **confiança** (margem do softmax) usada pelo limiar de escalada a humano (doc 09, §6). diff --git a/docs/matematica/07-atencao-mista-kvcache.md b/docs/matematica/07-atencao-mista-kvcache.md new file mode 100644 index 0000000000000000000000000000000000000000..4f5b1d72ef85b1e0b5c536aec12e2abb0371323d --- /dev/null +++ b/docs/matematica/07-atencao-mista-kvcache.md @@ -0,0 +1,50 @@ +# 07 — Atenção: mistura de mecanismos, √d, atenção linear e KV-cache + +**Módulos:** `percepcao/atencao.py` (MixtureOfAttention), `nucleo/modelo.py` (KV-cache). +O modelo combina **mais de um mecanismo de atenção** (item 10 do escopo): global (softmax +completo), janela deslizante (sliding window) e linear (kernel) — com **gating aprendido**. + +## 1. Por que escalar por $\sqrt{d_k}$ (prova) + +**Hipóteses.** Componentes de $q, k$ i.i.d. com média 0, variância 1 (pós-LayerNorm com +pesos inicializados ortogonalmente). +**Afirmação.** $\mathrm{Var}(q^\top k) = d_k$, logo $q^\top k/\sqrt{d_k}$ tem variância 1. +**Demonstração.** $\mathrm{Var}(q^\top k)=\sum_{i=1}^{d_k}\mathrm{Var}(q_i k_i)=\sum_i \mathrm{E}[q_i^2]\mathrm{E}[k_i^2]=d_k$ (independência; média zero elimina o termo cruzado). Sem a +normalização, as pontuações têm desvio $\sqrt{d_k}$ e o softmax satura (gradientes +$\approx 0$): a escala evita o beco sem saída do gradiente anulado. $\blacksquare$ + +## 2. Complexidades e cota da mistura + +Seja $n$ o comprimento da sequência, $d$ o modelo. Custo FLOPs por cabeça: +- **Global:** $O(n^2 d_k)$ — toda a matriz $QK^\top$. +- **Janela** de raio $r$: cada consulta vê $\le 2r+1$ chaves ⇒ $O(n\,(2r{+}1)\,d_k) = O(n r d_k)$. +- **Linear** com feature map $\varphi:\mathbb{R}^{d_k}\to\mathbb{R}^m$: computa-se + $\big(\varphi(Q)(\varphi(K)^\top V)\big)$ — **prova de associatividade** que muda a ordem + das matrizes: $\sum_i \varphi(q)^\top\varphi(k_i) v_i = \varphi(q)^\top\big(\sum_i \varphi(k_i) v_i^\top\big)$; + logo $O(n\,m\,d_k + n\,m\,d_v)$ — linear em $n$. Usamos $\varphi(x)=\mathrm{elu}(x)+1$ + (não-negatividade necessária para a interpretação probabilística de Katharopoulos et al. 2020). + +**Teorema 7.1 (custo do gating como mistura convexa).** Se o gate $g$ sobre os $J$ mecanismos +satisfaz $\sum_j g_j = 1,\ g_j\ge 0$, o custo esperado do MixtureOfAttention é +$\sum_j g_j C_j$ — limitado pela mistura convexa dos custos; a camada pode reduzir custo +deslocando massa do mecanismo global ($C=n^2d_k$) para o linear ($C=nm d_k$) sem sair do +simplex (restrito em `atencao.py` por `softmax` sobre logits do gate). +**Demonstração.** Linearidade da esperança sobre a execução por cabeça com pesos $g_j$. $\blacksquare$ + +## 3. KV-cache: decodificação $O(1)$ por token + +Autoregressão com cache: guardam-se $K, V$ históricos ($n_{kv}\times d_k$ cada). No passo $t$, +a nova consulta custa $O(t\,d_k)$ leituras+FLOPs (uma linha de $Q$ contra $t$ linhas de $K,V$) — +por token, custo constante; sem cache seria $O(t^2 d_k)$ reprocessar tudo. +**Memória:** $2\,n_{kv}\,H\,d_k$ floats — para $n_{kv}=512$, $H=4$, $d_k=48$ (config CPU): +$2\cdot512\cdot4\cdot48\cdot4\text{B} \approx 786\,\text{kB}$ por requisição. O cache é +**recortado por janela** (máx. `n_kv`) — coerente com a otimização de RAM do item 1. + +## 4. Multi-modal: fusão por cross-attention + +Embeddings por modalidade $z_m = \mathrm{Enc}_m(x_m)$ (texto, imagem, áudio, vídeo, tabular) +projetados a $d$ comum; a fusão usa cross-attention +$$z = \mathrm{MHA}(Q=W_q z_{\text{texto}},\; K=W_k z_{\text{todos}},\; V=W_v z_{\text{todos}}) + z_{\text{texto}} \tag{7.1}$$ +mais **gating por modalidade** $\pi_m = \mathrm{softmax}_m(a_m)$ com $a_m$ aprendido e +condicionado à presença da modalidade — modalidade ausente recebe gate $0$ (máscara). +A prova de estabilidade segue do Teorema 1 (norma controlada) + LayerNorm pré-atuação. diff --git a/docs/matematica/08-quantizacao-w8a8.md b/docs/matematica/08-quantizacao-w8a8.md new file mode 100644 index 0000000000000000000000000000000000000000..ad709fc100e33f0e90ef53308975cc970661bd04 --- /dev/null +++ b/docs/matematica/08-quantizacao-w8a8.md @@ -0,0 +1,113 @@ +# 08 — Quantização W8A8: cotas de erro e treino consciente de quantização + +**Módulo:** `quanta/quantizacao.py`. Pesos e ativações em 8 bits (item 9 do escopo), +com fallback puro-PyTorch quando `bitsandbytes` não dispõe de backend CPU/CUDA. + +## 1. Quantização simétrica W8 + +Dado tensor real $X$, com escala por tensor (ou por canal de saída): +$$s \;=\; \frac{\max|X|}{127}, \qquad Q(X) = \mathrm{clamp}\Big(\mathrm{round}\big(X/s\big),\;-127,\;127\Big), \qquad \tilde X = s\,Q(X). \tag{8.1}$$ + +**Teorema 8.1 (cota suprema do erro).** $\|\tilde X - X\|_\infty \le s/2$. +**Demonstração.** $\mathrm{round}(u)$ muda $u$ por no máximo $1/2$; o *clamp* só atua fora de +$[-127,127]$, onde $\mathrm{round}(X/s)=\pm127$ exatamente no limite, mudança $<1/2$ após +arredondamento; logo $|Q(u)-u|\le 1/2$ e $\|\tilde X - X\|_\infty = s\,\|Q(X/s)-X/s\|_\infty \le s/2$. $\blacksquare$ + +**Teorema 8.2 (erro médio).** Para $X$ com fração de massa uniforme dentro de cada célula de +arredondamento, $\mathbb{E}|\tilde X - X| \approx s/4$ (esperança de $|U|$, $U\sim U(-s/2,s/2)$). +**Uso:** a telemetria compara o erro observado com $s/4$; desvios grandes indicam outliers +densos (dispara escala por canal). + +## 2. Matmul inteira (caminho W8A8 real) + +$\tilde Y = \tilde X \tilde W^\top = s_x s_w\, \big(Q(X)\,Q(W)^\top\big)$. Em hardware com +int8 GEMM (CUDA/bnb) a soma acumula em int32; em CPU (nossa execução) faz-se o produto em +`int32` via `torch` (conversão explícita) ou **simulação fake-quant** em fp32 — o erro segue +a cota do Teorema 8.1 em ambos os casos, pois a simulação aplica as mesmas funções (8.1). + +## 3. QAT — straight-through estimator (STE) + +Na fase de treino quantizado, o passo de quantização tem gradiente nulo quase sempre +(escadinhas); usa-se STE (Bengio et al., 2013): forward com $\tilde X$, backward com +identidade. + +**Teorema 8.3 (STE é subgradiente válido).** A função $q(u)=s\,\mathrm{clamp}(\mathrm{round}(u/s),-127,127)$ +é Lipschitz com constante 1 (composição de 1-Lipschitz: clamp e round são 1-Lipschitz; +por Rademacher, $q$ é diferenciável a.e. com $|q'|\le 1$; o conjunto de descontinuidades tem +medida nula). O STE substitui $q'$ por $1$ — um subgradiente admissível no sentido de Clarke, +e o SGD com subgradientes converge para um mínimo local sob Robbins–Monro (Teorema 1.2). $\blacksquare$ + +## 4. LLM.int8() (bitsandbytes) — decomposição de outliers + +Dettmers et al. (2022): separar colunas de ativação com magnitude acima de limiar $\tau$: +$$X W^\top \;=\; \underbrace{X_{:,O} W_{O,:}^\top}_{\text{fp16, colunas de outliers } O} \;+\; \underbrace{X_{:,I} W_{I,:}^\top}_{\text{int8, } I=O^c}. \tag{8.2}$$ +**Cota de erro:** o erro da parte int8 é limitado por $\tfrac{s_x s_w}{2}\|W_{I,:}\|_1$ +(Teorema 8.1 + subaditividade), e os outliers — únicos responsáveis por explosões de erro — +ficam exatos em fp16. Implementação: se `bitsandbytes` disponível com backend funcional, +delega; senão, o mesmo critério ($\tau = 6\,\mathrm{MAD}$) é aplicado manualmente. + +## 5. Onde o KHTST aplica + +1. **Inferência/avaliação:** pesos do decoder quantizados W8 após o treino (compara-se + perplexidade fp32 × W8 — relatada na telemetria). +2. **Treino (opcional, config):** QAT-STE nas projeções lineares nos últimos passos. +3. **bitsandbytes:** integração condicionada a CUDA; no ambiente CPU usa-se o caminho + simétrico puro-PyTorch com as mesmas cotas (honestidade documentada no README). + +## 5. v4 — Escalas por grupo (Teorema 8.4) + +Divide-se a última dimensão em grupos de tamanho $g$ (64) com escala própria +$s_g = \max|x|_g/q_{\max}$. + +**Teorema 8.4 (erro estruturalmente menor).** Com escalas por grupo, o erro +$\infty$-norma por elemento é $\le s_g/2$ e, para entradas aproximadamente +uniformes dentro de cada grupo, $\mathbb{E}[\mathrm{erro}^2] \approx s_g^2/12 +\le (s_{\text{tensor}}/g^{1/d})^2/12$ — redução de raiz quadrada em $g$ no caso +1D ($\approx\sqrt{g}$× menos erro RMS que a escala por tensor). + +**Demonstração.** Arredondamento simétrico: $|\mathrm{erro}|\le s/2$ por célula +(mesmo argumento do Teorema 8.1 restrito ao grupo). Para $x$ uniforme em +$[-s/2, s/2]$ dentro do grupo, $\mathbb{E}[\mathrm{erro}^2] = s^2/12$; como +$s_g$ usa o máximo de APENAS $g$ elementos (vs. todo o tensor), $s_g \le +s_{\text{tensor}}$ com igualdade apenas quando o máximo global está no grupo — +em média, sobre grupos com distribuição semelhante, a razão dos máximos é +$O(1/\sqrt{g})$ para subamostragem aleatória, dando o fator $\sqrt{g}$. $\blacksquare$ + +## 6. v4 — Correção de viés pós-quantização (Teorema 8.5) + +**Teorema 8.5 (eliminação do desvio de 1ª ordem).** Seja +$\Delta b = \mathbb{E}_a[a\,W^\top] - \mathbb{E}_a[a_q W_q^\top]$ calibrado em +um lote representativo. Então $y_{\text{corr}} = y_q + \Delta b$ tem +$\|\mathbb{E}[y_{\text{fp}} - y_{\text{corr}}]\| \le \|\,\mathrm{Cov}\,\|\cdot +\varepsilon_2$ — o erro MÉDIO restante é de 2ª ordem (Teorema 8.2: média $s/4$ +por produto interno). + +**Demonstração.** $y_{\text{fp}} - y_q = a W^\top - a_q W_q^\top = (a-a_q)W^\top ++ a_q (W-W_q)^\top - (a-a_q)(W-W_q)^\top$; tomando esperança, os dois primeiros +termos são exatamente $\Delta b$ (removido pela correção); resta o produto +cruzado, quadrático nos erros de quantização — $O(\varepsilon^2)$. $\blacksquare$ + +Implementação: `quantiza_por_grupo` (fallback automático p/ dim < g) e +`FakeQuantW8A8Grupo.calibrar` → `correcao_vies` (GPTQ-lite). + +## §§7–9 (v5) — Quantização REAL sem fakes + +Refactor v5 (`quanta/quantizacao.py`): `FakeQuantW8A8`, `FakeQuantW8A8Grupo` e +`aplicar_w8_linear` REMOVIDOS. A inferência NUNCA é simulada — os backends reais: + + §7.1 torchao `Int8DynamicActivationInt8WeightConfig` — kernels int8 reais (CPU/GPU); + §7.2 torch.ao.quantization `prepare_qat`→`convert` — Treinamento INT8 UNIVERSAL + (backend x86/fbgemm/qnnpack; conversão final REAL, fluxo do pedido); + §7.3 torchao.float8 `convert_to_float8` — treino nativo FP8 (GPU H100+; degrada + honesta em CPU); + §7.4 bitsandbytes `load_in_8bit=True` — weight-only 8-bit GPU (integração HF). + +**Teorema 16.7 (equivalência).** Simulação round→clamp→dequantize e kernel int8 +real implementam o MESMO mapa afim ⇒ erro idêntico; as cotas 8.1–8.5 valem ao +kernel. A diferença é VELOCIDADE/MEMÓRIA (Teorema 16.8: ≈3,76× com escalas por +grupo), não erro. +**§8 métricas honestas:** `metricas_qualidade` (erro relativo, SNR dB, erro máx). +**§9 diagnóstico:** `modo_quantizacao()` — disponibilidade real por backend. +Durante o TREINO mantém-se QAT com STE (Teorema 8.3) — classe `QATW8A8` +(metodologia idêntica ao prepare_qat, escalas por grupo) — mas a conversão de +exportação é sempre REAL (§7.1/§7.2). diff --git a/docs/matematica/09-ortogonais-cooperativas-punicao-telemetria-pdca.md b/docs/matematica/09-ortogonais-cooperativas-punicao-telemetria-pdca.md new file mode 100644 index 0000000000000000000000000000000000000000..821f8dc6f2c4085b197c4803258b789d8b2d6d8c --- /dev/null +++ b/docs/matematica/09-ortogonais-cooperativas-punicao-telemetria-pdca.md @@ -0,0 +1,98 @@ +# 09 — Camadas ortogonais e cooperativas; punição/retreino; telemetria; ciclo PDCA + +## 1. Camadas ortogonais (Householder) + +**Módulo:** `percepcao/ortogonais.py`. Uma camada linear com $W$ ortogonal satisfaz +$W W^\top = I$. Implementação parametrizada por reflexões de Householder +$$H(v) = I - 2\frac{v v^\top}{\|v\|^2}, \qquad W = \prod_{i=1}^{p} H(v_i), \tag{9.1}$$ +**Teorema 9.1 (ortogonalidade exata por construção).** $H(v)H(v)^\top = +(I - 2\hat v\hat v^\top)(I - 2\hat v\hat v^\top) = I - 4\hat v\hat v^\top + 4\hat v(\hat v^\top\hat v)\hat v^\top = I$, +pois $\hat v^\top \hat v = 1$. Produto de ortogonais é ortogonal. $\blacksquare$ +**Custo:** $p$ reflexões custam $O(p d^2)$ no geral, mas como produto vetor-matriz +sucessivo: $O(p\,d^2)$ → com $p=d$ (casa completa) $O(d^3)$ é caro; usamos $p\in\{1,2\}$ +(ortogonalidade parcial suficiente) ou projeção QR periódica da base de pesos (cada +$N$ passos, custo $O(d^2)$ amortizado). + +**Teorema 9.2 (gradiente estável).** Se $W$ ortogonal, $\|Wx\|_2=\|x\|_2$ e o número de +condição $\kappa(W)=1$; o gradiente que retropropaga por $W$ tem norma preservada: +$\|W^\top g\|=\|g\|$ — elimina explosão/vanishing associados a $\sigma_{min}\ll\sigma_{max}$. +**Uso:** nas camadas de **fusão multimodal**, onde escalas de modalidades diferentes +amenizariam o gradiente. + +## 2. Camadas cooperativas (mistura de expertos com balanceamento) + +$N$ expertos $\{E_i\}$ com gate $g=\mathrm{softmax}(a)$; saída $y=\sum_i g_i E_i(x)$. +Perda de balanceamento (Switch Transformer): +$$L_{bal} = N \sum_{i=1}^{N} f_i \cdot P_i, \qquad f_i=\tfrac{1}{B}\#\{x: i = \arg\max g(x)\},\; P_i = \overline{g_i}. \tag{9.2}$$ +**Teorema 9.3 (mínimo uniforme).** $L_{bal} \ge N\cdot\frac1N\cdot\frac1N = \frac1N$ com +igualdade sse $f_i=P_i=1/N$ (por desigualdade da média aritmética nas somas $\sum f_i=\sum P_i=1$). +Logo minimizar $L_{bal}$ força cooperação (uso equilibrado) em vez de colapso a um expert. $\blacksquare$ +**Cooperação ativa:** 2 expertos sempre ativos por token (top-2), exigindo divisão de trabalho +— combinada com $L_{bal}$, evita a solução degenerada "um expert faz tudo". + +## 3. Punição e retreino (item 6): detectar não-aprendizado e reagir + +**Detector de estagnação:** janela deslizante de $W_s$ passos sobre a perda de validação +$\ell_t$; estagnado se +$$\frac{\min_{t-W_s0.25$ = deslocamento grave. +4. **EQ/TE dos SOMs** (doc 01 §5) — evolução da memória auto-organizável. +5. **Saúde do treino:** combinador booleano ponderado (estagnação, gradiente fora de + $[10^{-7},10^{2}]$, NaN/Inf, PSI, rank efetivo) → classificação de **bug** × **ruído** + (doc qualidade). + +## 5. Contratos de qualidade — responsabilidade do Agente Engenheiro + +**Regras de classificação (revisadas pelo Agente Engenheiro antes de cada modificação, +com garantia de monotonia de capacidade — nenhuma alteração pode remover uma +funcionalidade existente):** +- **Bug (invariante violado):** formas incompatíveis, NaN/Inf, norma de gradiente explosiva, + $\mathrm{rank}_{eff}$ colapsando, contexto RSOM acima da cota do Teorema 5.3, erro de + quantização acima de $s/2$ (violaria o Teorema 8.1 — impossível em código correto). +- **Erro de digitação que causa falha de lógica:** identificador definido mas nunca usado / + usado uma única vez em comparação com igualdade sempre-falsa, string de configuração + desconhecida ("cuda"×"cpu"), constantes duplicadas divergentes — heurísticas estáticas + (AST) + dinâmicas (asserts de sanidade) — reportadas **separadas** dos bugs. +- **Ruído (não é bug):** perda com alta variância mas mediana estável; gradiente esparso + em minibatches pequenos. + +## 6. Ciclo de raciocínio cíclico PDCA (item 3) — limiar de escalada humana + +Estados: **Planejar** → **Distribuir** → **Executar** → **Feedback** → (redistribuir | planejar | entregar). +A decisão de entregar vs. redistribuir é um **teste de razão de verossimilhança** sobre a +confiança $p$ do passo (probabilidade posterior de estar correto, estimada por S-SOM+entropia): +escalada ao humano se $p < \tau$, com +$$\tau^\star = \frac{C_{falso\,positivo}}{C_{falso\,positivo} + C_{falso\,negativo}} \tag{9.4}$$ +**Teorema 9.5 (limiar bayesiano ótimo).** Minimizar o custo esperado $\mathbb{E}[C] = +p\,C_{FN}(1-a) + (1-p)C_{FP}a$ sobre a ação binária $a\in\{\text{entregar}=1,\text{escalar}=0\}$ +dá $a=1$ sse $p > C_{FP}/(C_{FP}+C_{FN}) = \tau^\star$. +**Demonstração.** $a=1$ custa $(1-p)C_{FP}$; $a=0$ custa $p C_{FN}$; comparar. $\blacksquare$ +Redistribuição interna (novos módulos/ferramentas) ocorre quando $p\ge\tau^\star$ mas a +incerteza $H(\pi)$ do gate é alta — custo esperado da tentativa adicional é limitado por +orçamento $B$ de passos (busca best-first com orçamento). diff --git a/docs/matematica/10-moe-foco-denso-som-v2.md b/docs/matematica/10-moe-foco-denso-som-v2.md new file mode 100644 index 0000000000000000000000000000000000000000..4e4720ac8ed020ad5b4cccd6f4318ef4e92cac55 --- /dev/null +++ b/docs/matematica/10-moe-foco-denso-som-v2.md @@ -0,0 +1,291 @@ +# 10 — MoE Agrupável com Foco na Tarefa e o Pipeline Denso→SOM (KHTST v2) + +Este documento fundamenta matematicamente as inovações da v2 antes da implementação: +(a) MoE **agrupável** com otimização por foco na tarefa — dar ênfase ao que é importante +e ignorar, por compactação e indexação, o que é irrelevante; (b) o pipeline em duas +fases **denso→SOM** — a rede aprimorada treina todos os parâmetros com o máximo de +conexões e só depois os estados ocultos são organizados pelo mapa de Kohonen com o +máximo de neurônios ativos; (c) predição multi-token (MTP) com pesos α aprendíveis; +(d) DPO com β adaptativo; (e) cirurgia de gradiente (PCGrad); (f) barreira analítica +de taxa de aprendizado (ABMO); (g) SmoothQuant com α aprendível; (h) punição dinâmica +de repetição. Reaproveitamos e corrigimos defeitos identificados no estudo do código +BiGRU_T_version, CNN-BiGRU e HAKO-v1/v3 do autor. + +## 0. Notação adicional (sobre o doc 00) + +- Especialistas: E = {e_1, …, e_N}, N = G·E_g, com grupos g(e) ∈ {1..G}. Grupos na v2: + **texto** (lm, noticia, pontuacao, instrucao, tts), **visual** (vqa, ocr, + imagem_caption), **áudio** (asr com características reais). +- Expert e é um MLP SwiGLU f_e: R^d → R^d com âncora (protótipo) μ_e ∈ R^d. +- Compactação de contexto: x̄(X) = (1/T)Σ_t x_t — o resumo do lote de tokens. +- Índice de roteamento: I = {μ_e, vieses de grupo, uso EMA} — O(N·d) escalares, + independente do tamanho de cada expert (é por isso que "ignorar" é barato: o + expert não selecionado nem é computado, e sua identidade é só uma linha no índice). +- Tarefa τ ∈ T = {lm, noticia, pontuacao, instrucao, tts, vqa, ocr, imagem_caption, asr}. + +## 1. MoE agrupável com foco na tarefa + +### 1.1 Definição (gate duplo: afinidade + viés de tarefa) + +Dado o lote de estados ocultos X ∈ R^{B×T×d}, o score do expert e para a amostra b é + + s_{b,e} = ⟨x̄_b, μ_e⟩ / √d + b_{g(e), τ} (10.1) + +onde b ∈ R^{G×|T|} é uma tabela aprendida de viés de tarefa. O gate por amostra é + + g_{b,e} = softmax_top-k( s_{b,·} )_e (fase foco), g = softmax(s) (fase densa) (10.2) + +e a saída é y_b = Σ_e g_{b,e} f_e(x_b). Na **fase densa** (item d — máximo de +conexões) todos os experts participam com peso softmax completo: cada parâmetro +recebe gradiente. Na **fase foco** apenas os k = 2 melhores por amostra são +computados; experts fora do top-k e fora do grupo da tarefa corrente **não são +sequer calculados** (ignorar por indexação, Proposição 10.4). + +### 1.2 Teorema 10.1 (não-interferência do viés de tarefa no top-k) + +Sejam s_(1) ≥ … ≥ s_(N) os scores ordenados de uma amostra e δ = s_(k) − s_(k+1) +a lacuna de Voronoi na fronteira do top-k. Se |b_{g,τ}|_∞ ≤ δ/2 para todos os +grupos g e a tarefa τ, então o conjunto top-k com viés é igual ao conjunto sem viés. + +**Prova.** Para qualquer par e_i ∈ top-k, e_j ∉ top-k temos s_i + b_i ≥ s_(k) − δ/2 +e s_j + b_j ≤ s_(k+1) + δ/2 = s_(k) − δ/2. Logo s_i + b_i ≥ s_j + b_j, com desempate +impossível pois a desigualdade é estrita quando |b|_∞ < δ/2. A ordem relativa dentro +do top-k pode mudar (afetando apenas os pesos g, que são contínuos em s + b), mas o +**conjunto** selecionado não muda. ∎ + +Corolário (garantia de foco): ampliar o viés do grupo da tarefa além de δ/2 pode +trocar especialistas, mas o dano máximo de QE de roteamento é limitado — o análogo +do Teorema de punição restrita a empates do HAKO-v3 (QE excesso ≤ √(1+γ) − 1): +aqui, se ‖b‖_∞ ≤ γ·d_min/2 com d_min a menor lacuna entre scores consecutivos +distintos, a perda relativa de afinidade é ≤ γ. + +### 1.3 Teorema 10.2 (equilíbrio de carga agrupado) + +Com a perda de balanceamento estilo Switch (Fedus et al. 2022) aplicada por grupo, + + L_lb = (1/G) Σ_g G_g · Σ_{e∈g} f_e · p_e, f_e = fração de amostras roteadas a e, + p_e = fração média do gate, (10.3) + +o mínimo L_lb = 1 (com N_g = E_g experts por grupo e roteamento uniforme). Sob +descida de gradiente com passo somável, f e p convergem ao produto uniforme dentro +de cada grupo. **Prova.** f_e, p_e ≥ 0 com Σ_{e∈g} f_e = 1, Σ_{e∈g} p_e = 1 (softmax); +pela desigualdade de Cauchy–Schwarz, Σ f_e p_e ≥ (Σ√(f_e p_e))²/N_g ≥ 1/N_g, com +igualdade sse f_e = p_e = 1/N_g. O gradiente de L_lb em relação aos scores empurra +p para 1/N_g (cálculo padrão do Switch), e o roteamento segue p. ∎ + +### 1.4 Teorema 10.3 (ortogonalidade intra-grupo reduz redundância) + +Com φ_e = média das ativações do expert e no lote normalizada (φ̂_e = φ_e/‖φ_e‖), +a penalidade L_ort = Σ_{g} Σ_{p ε_h na janela seguinte. Por indução nas +janelas, o número de mortos decresce a não ser que a cobertura de dados seja +menor que K — caso em que δ é a fração de células não-visitadas, limite +informacional e não algorítmico. ∎ + +**(c) Orçamento de crescimento** (GSOM/GCS): o alvo de "máximo de neurônios +ativos" cap o crescimento quando taxa_ativos ≥ alvo (0.90) OU orçamento +esgotado — crescimento além disso degrada QE por fragmentação. + +### 2.4 Métrica da fase + +taxa_ativos = |{j : h_j ≥ ε_h}| / K por variante; EQ, TE, rank efetivo do uso +(doc 01 §5). Telemetria: `som/taxa_ativos_{nome}`. + +## 3. Predição multi-token (MTP) com α aprendíveis + +Cabeças K=2: h^{(m)}_t = SiLU(W_m h_t), logits^{(m)} = E·h^{(m)}_t (com E = tabela +de embeddings empatada — sem parâmetros de vocabulário extra, corrigindo o custo +K·V·d = 65M do Medusa do CNN-BiGRU). Perda com deslocamento m: + + L_m = CE(logits^{(m)}_{0:T−m}, alvo_{m:T}), L_MTP = Σ_m α_m L_m − β_ent·H(α) (10.4) + +**Teorema 10.8 (anti-colapso dos α).** O minimizador de Σ α_m L_m − β H(α) sobre +o simplex é α*_m ∝ exp(−L_m/β); com β → 0+, colapso winner-takes-all; com β > 0, +α*_m/α*_n ≥ exp(−(L_m − L_n)/β) — a razão entre pesos é limitada, impedindo que +uma única cabeça absorva todo o gradiente. **Prova.** Lagrangiano do simplex; +condição de KKT dá log α_m = −L_m/β + const. A desigualdade segue por divisão. ∎ + +## 4. DPO com β adaptativo (corrigindo o sinal da dualidade) + +Dado o par (escolhido = saída dourada, rejeitado = corrupção por aumento — dropout/ +embaralhamento de tokens do doc de estudo), com logps de sequência na região de +resposta: + + L_DPO = −log σ( β·[(π_c − ref_c) − (π_r − ref_r)] ) (10.5) + β_{t+1} = β_t · exp( −η_β·(KL̄_t − K_alvo) ) (10.6) + +**Proposição 10.9 (ponto fixo da dualidade).** O passo (10.6) é um gradiente em +log β do lagrangiano KL̄(β) com multiplicador; seu ponto fixo satisfaz KL̄ = K_alvo; +com η_β pequeno, |KL̄_t − K_alvo| é limitado por O(η_β + 1/β_t·ΔKL). (Esta é a +correção do sinal documentado no estudo: β deve DIMINUIR quando KL excede o alvo, +soltando a âncora de referência.) ∎ + +## 5. Cirurgia de gradiente (PCGrad de duas perdas) + +Dado g₁ = ∇L_ce e g₂ = ∇L_aux: se ⟨g₁, g₂⟩ < 0, substitua g₂ por +g₂ − (⟨g₁,g₂⟩/‖g₁‖²)·g₁ (projeção no complemento ortogonal de g₁), com guarda +‖g₁‖² > ε. + +**Proposição 10.10.** Após a projeção, ⟨g₁, g₂'⟩ = 0, logo o passo combinado +g₁ + g₂' não aumenta L_ce em primeira ordem (a componente de g₂ contra o +gradiente principal foi removida). ∎ + +Implementação: dois `torch.autograd.grad` com `retain_graph=True` na primeira +chamada (as perdas compartilham o grafo do único forward) — contratado como no +estudo do BiGRU_T, mas com `set_to_none` e atribuição por parâmetro. + +## 6. Barreira analítica de LR (ABMO — "a rede propõe, a analítica corta") + +Proxy de curvatura por inflação de perda (adimensional): +L̂_t = clip(0.9·L̂_{t−1} + 0.1·(|L_t|/L_ref), 0.05, 500), L_ref fixado no 1º passo. + +**Teorema 10.11 (garantia de descida).** Se L é L-suave, o passo de gradiente com +η_eff ≤ (2 − m)/L̂ tem redução garantida L(θ_{t+1}) ≤ L(θ_t) − η_eff(1 − L̂η_eff/2)‖∇L‖² +para qualquer L̂ ≥ L real. Com m = 0.25, η_eff = min(lr, 1.75/L̂). **Prova.** Desigualdade +de descida padrão para funções L-suaves com passo η < 2/L; usar L̂ ≥ L só relaxa o +passo, preservando a condição. ∎ + +Correção do defeito do HAKO-v1 aqui documentado: L̂ usa a PERDA (inflação), não +a "norma de gradiente = perda" sem sentido físico; e a barreira é aplicada +efetivamente ao lr (não apenas registrada). + +## 7. SmoothQuant com α aprendível (STE) + +Escalas por canal (Xiao et al. 2023): s_j = max|X_j|^α / max|W_j|^{1−α}. A +invariância Y = XWᵀ = (X/s)(sW)ᵀ é exata. Com α = σ(η): + + ∂s_j/∂η = s_j·σ'(η)·(log max|X_j| − log max|W_j|) (10.7) + +**Proposição 10.12.** O gradiente do erro E(α) = ‖Y − Y_q‖²_F/‖Y‖²_F em η, com +quantização STE (round com backward identidade), é dado por (10.7) encadeado com +∂E/∂s via os resíduos por canal; o passo de gradiente decresce E monotonamente +enquanto ‖∇E‖ não zera. ∎ + +Correções dos defeitos do estudo: erro medido pós-step (não pré — bug de +convergência do w8a8_error_reduction); buffers registrados (não atributos Python); +α clamp [0.05, 0.95]. + +## 8. Punição dinâmica de repetição na geração + +Com contagens c(v) do contexto e entropia H do contexto: + + pen(v) = 1 + (base − 1)·1[c(v) ≥ 1]·min(1, H/H_max), base = 1.2 (10.8) + logits(v) ← logits(v)/pen(v) (10.9) + +**Proposição 10.13.** (i) Tokens nunca vistos não são penalizados (indicador +1[c≥1]); (ii) quanto mais incerto o modelo (H alta), mais agressiva a penalidade +— corrigindo loops repetitivos exatamente quando o modelo insiste; (iii) em +log-space, (10.9) é subtração de δ_v = log pen(v) ∈ [0, log 1.2]. ∎ + +## 9. Protocolo de estados no HuggingFace (item c — poupar armazenamento) + +- **Serialização**: safetensors (sem pickle — segurança), um arquivo por estado: + checkpoints LOCAIS `estados_local/{tag}/modelo.safetensors` + `meta.json` (passo, época, + perda, versão, hash SHA-256 do arquivo). +- **Atomicidade**: `upload_file` do hf_hub é um commit único no repo — um estado + visível é sempre completo. Deleção local somente após commit confirmado. +- **Retomada**: `carregar(tag)` baixa e valida o hash antes de `load_state_dict`. +- **Ciclo**: época concluída → push → remover checkpoint local → disco volta ao + mínimo; o próximo segmento baixa o último estado ("salvar (e usar)"). +- **Ordem canônica**: tags `epoca-{n:03d}` e `fase-som` — listagem lexicográfica + = cronológica. + +## 10. Resumo dos vínculos prova→código + +| Resultado | Implementação | +|---|---| +| 10.1–10.4 | `percepcao/moe.py` (gates, viés de tarefa, máscara de foco) | +| 10.5 | `treino/treinador.py` (alinhar_som com stop-grad + fase SOM) | +| 10.6–10.7 | `memoria/som_base.py` (empate restrito, reseeding, taxa_ativos) | +| 10.8 | `treino/mtp.py` (α softmax + H(α)) | +| 10.9 | `treino/dpo.py` (β em log-space, sinal corrigido) | +| 10.10 | `treino/cirurgia_grad.py` | +| 10.11 | `treino/treinador.py` (barreira LR) | +| 10.12 | `quanta/quantizacao.py` (α-STE) | +| 10.13 | `nucleo/modelo.py::gerar` | +| §9 | `dados/checkpoints.py` | diff --git a/docs/matematica/11-composicao-microunidades-v3.md b/docs/matematica/11-composicao-microunidades-v3.md new file mode 100644 index 0000000000000000000000000000000000000000..182f72a41a149d58d732e1322bf764cc280cc822 --- /dev/null +++ b/docs/matematica/11-composicao-microunidades-v3.md @@ -0,0 +1,386 @@ +# 11 — Composição de Microunidades v3: Transformers + CNN‑BiGRU + BiGRU, memória interna e controles adaptativos + +> **Princípio v3 (regra do projeto): matemática ANTES dos scripts.** +> Este documento define e **prova** cada mecanismo novo antes da implementação em +> `src/khtst/`. Notação herdada do doc 00; docs 09–10 continuam válidos (ortogonais, +> MoE denso→SOM, punição/retreino). Estudo-fonte: `xavante_work/flexnet/*` e +> `xavante_work/xavante/model/*` do repo `PowerMachine/gru-ring-v13-9-2` (16 arquivos), +> com correções de defeitos identificados (§11). + +--- + +## 0. Definições: microunidade e composição + +**Definição 0.1 (microunidade).** Uma *microunidade* é um módulo paramétrico +`u_θ: R^d → R^d` com **competência declarada** `c(u) ∈ {conv, rec, att, rot, mlp}` e +custo `F(u)` (FLOPs por token). A população `U = {u_1, …, u_n}` é **escalável**: `n` +cresce/diminui em tempo de treino pelo gestor de crescimento (§5). + +**Definição 0.2 (topologias).** Dado `x ∈ R^d` e unidades `u_1…u_n`: + +- **Serial (S):** `y = x + Σ_k γ_k · u_k(x_k)`, com `x_1 = x`, `x_{k+1} = x_k + γ_k u_k(x_k)` — resíduos encadeados com ganhos `γ_k > 0`. +- **Paralela (P):** `y = x + Σ_k α_k(x) · u_k(x)`, `α(x) = softmax(g(x)/τ)` — ramos executados sobre a MESMA entrada e agregados por gating convexo. +- **Isolada (I):** `y = x + Σ_k β_k · u_k(x)` com **subconjuntos disjuntos de parâmetros e gradiente bloqueado entre ramos** (`u_i` não vê gradientes de `u_j, j≠i`; sem estado compartilhado) — especialistas independentes agregados por pesos fixos ou por votação. +- **Recursiva (R):** `y_{k+1} = y_k + γ^k · u(y_k)` com `y_0 = x`, profundidade efetiva cortada quando `‖γ^k u(y_k)‖ ≤ ε_stop`. + +As quatro topologias podem **aninhar-se** (ex.: stem serial de P, ramos paralelos de S…), formando o *compositor*. + +--- + +## 1. Teorema (estabilidade da composição serial) + +**Teorema 1.1 (serial não amplifica gradiente).** Seja cada unidade pré‑norma +`u_k(x) = W_k σ(LN(x))` com Lipschitz `L(u_k) ≤ L_k`. A função serial com resíduo +`F = Id + Σ γ_k u_k∘…∘u_1` satisfaz, para a derivada em qualquer ponto, + +``` +‖∂F/∂x‖_op ≥ 1 − Σ_{k=1..n} γ_k L_k (cota inferior — sem desaparecer) +‖∂F/∂x‖_op ≤ 1 + Σ_{k=1..n} γ_k L_k (cota superior — sem explodir) +``` + +*Prova.* Por indução: `F_1 = Id + γ_1 u_1 ⇒ ∂F_1 = I + γ_1 ∂u_1`; pela desigualdade +triangular inversa `‖I + A‖ ≥ ‖I‖ − ‖A‖ = 1 − ‖A‖` e direta `≤ 1 + ‖A‖`. Passo +indutivo: `F_{k+1} = F_k + γ_{k+1} u_{k+1}∘F_k ⇒ ∂F_{k+1} = (I + γ_{k+1} ∂u_{k+1}(F_k)) · ∂F_k`; +o fator esquerdo tem norma em `[1 − γ_{k+1}L_{k+1}, 1 + γ_{k+1}L_{k+1}]` e multiplica a +cota anterior. ∎ + +**Corolário 1.2 (condição de projeto).** Com `γ_k = 1/√n` e `L_k ≤ L`, o caminho de +gradiente serial permanece em `[1 − L√n, 1 + L√n]` — para `n ≤ (1/L)²` a serialização +é bem‑condicionada. Implementação: pré‑norma + ganho `1/√n`. + +--- + +## 2. Teorema (paralela reduz a constante de Lipschitz; cobertura de campos receptivos) + +**Teorema 2.1 (paralela convexa).** Com gating convexo `α_k(x) ≥ 0, Σα_k = 1` e +`F(x) = x + Σ α_k(x) u_k(x)`: + +``` +Lip(F) ≤ 1 + max_k Lip(u_k) + Lip(α)·max_k sup‖u_k‖ +``` + +*Prova.* Para `x, x'`: `‖F(x) − F(x')‖ ≤ ‖x − x'‖ + ‖Σ α_k(x)u_k(x) − α_k(x')u_k(x')‖`. +Decompondo (identidade `Σα_k = 1`): + +``` +Σ α_k(x)(u_k(x) − u_k(x')) + Σ (α_k(x) − α_k(x')) u_k(x') + ≤ max_k L_k · ‖x − x'‖ + ‖α(x) − α(x')‖₁ · max_k sup‖u_k‖. +``` + +e `‖α(x) − α(x')‖₁ ≤ Lip(α)‖x − x'‖`. ∎ + +**Contraste com serial:** serial multiplica constantes (`Π(1+γL)`), paralela toma o +**máximo** — por isso ramos paralelos são o lugar seguro para as famílias de maior +Lipschitz (atenção global). + +**Proposição 2.2 (cobertura mínima).** Sejam os campos receptivos +`R_conv = O(k)` (kernel k, local), `R_bigru = O(T)` (recorrência bidirecional cobre +toda a sequência com custo O(T)), `R_att = O(T)` (global com custo O(T²) — mitigado +O(T) no híbrido doc 07). A união dos três tipos cobre tarefas de (i) n‑gramas locais, +(ii) ordem/reversibilidade sequencial, (iii) dependências longas — **e nenhuma família +cobre as outras**: conv não vê dependência de comprimento > k; GRU unidirecional não +vé contexto futuro; atenção pura não codifica ordem sem senoidal/RoPE. Logo a +composição mínima que cobre os três regimes contém **um ramo de cada família** (base +`n = 3`), com gating aprendendo a seleção por token. + +--- + +## 3. Teorema (isolada: redução de variância e proibição de interferência) + +**Teorema 3.1 (variância de ensemble isolado).** Sejam ramos `u_k` com gradientes +por amostra `g_k` com `E[g_k] = g`, `Cov(g_k, g_j) = ρσ² (k≠j), ρ ≥ 0` (compartilham +dados ⇒ correlação ρ; parâmetros disjuntos ⇒ covariâncias *de ruído de amostra* +independentes dado o dado). O estimador agregado `ĝ = (1/K)Σ g_k` tem + +``` +Var[ĝ] = σ²(ρ + (1−ρ)/K) → σ²ρ quando K→∞. +``` + +*Prova.* Cálculo direto da média de covariâncias: K termos de variância σ² e K(K−1) +de covariância ρσ², dividido por K². ∎ + +**Proposição 3.2 (critério de isolamento).** Se `cos(g_i, g_j) < 0` (interferência +negativa — PCGrad do doc 10 detecta), mover os ramos para topologia **isolada** +(remove a soma dos gradientes conflitantes) elimina a componente destrutiva: o +gradiente do agregado isolado por votação fixa é média de gradientes não combinados, +e o passo efetivo `Δθ = −η·Σ β_k g_k` com `β_k ≥ 0` fixos satisfaz +`⟨Δθ, g_alvo⟩ ≤ 0` sempre que `⟨g_k, g_alvo⟩ ≤ 0` para todo k (cada ramo só pode +ajudar a si mesmo; ninguém puxa contra). Na topologia compartilhada isso falha +porque um único parâmetro comum recebe `Σ g_k` e pode retroceder em `g_alvo`. + +**Corolário 3.3 (divisão de competências).** Ramos **isolados** são o lugar das +competências que não devem compartilhar parâmetros: encoders por modalidade +(imagem/áudio) e cabeças auxiliares — a interface do módulo permanece a mesma +(contratos §10), mas o gradiente é tubo‑a‑tubo. + +--- + +## 4. Teorema (recursiva: ponto fixo com profundidade finita) + +**Teorema 4.1 (convergência geométrica).** Seja `u` Lipschitz com constante `L_u` e +ganho decrescente `γ ∈ (0, 1/L_u)`. A iteração `y_{k+1} = y_k + γ^k u(y_k)` satisfaz + +``` +‖y_{k+1} − y*‖ ≤ ‖y_k − y*‖ + γ^k ‖u(y_k)‖, e com ‖u‖ ≤ B: +‖y_n − x‖ ≤ B(1 − γ^n)/(1 − γ) ≤ B/(1−γ) (comprimento total limitado) +``` + +A série de correções é absolutamente convergente ⇒ profundidade efetiva pode ser +**cortada** em `ε_stop` com erro total `≤ ε_stop/(1−γ)` (Banach/Weierstrass M‑test). + +*Prova.* Soma das normas das correções por comparação com série geométrica; o resto +da série após corte `Σ_{k≥n} B γ^k = Bγ^n/(1−γ) ≤ ε_stop/(1−γ)`. ∎ + +**Proposição 4.2 (por que recursiva nas microunidades).** A recursiva compartilha +**os mesmos pesos** `u` em profundidades variáveis — parâmetros O(1) para +refinamento O(k). É a topologia com melhor razão capacidade/parâmetro; usamos +`k ≤ 2`, `γ = 0.5`, `ε_stop = 10⁻²`. + +--- + +## 5. Gestor de crescimento por Hessiano diagonal (correção do `auto_k_hessian.py`) + +**Lema 5.1 (Hutchinson–Rademacher para a diagonal).** Para `H` simétrica e +`v ~ Rademacher` (entradas ±1, E[v]=0, E[vv^T]=I): + +``` +E[v ⊙ (Hv)] = diag(H) +``` + +*Prova.* `E[v_i Σ_j H_ij v_j] = Σ_j H_ij E[v_i v_j] = H_ii`. ∎ + +**Defeito corrigido do código estudado:** `auto_k_hessian.py` computa +`grad(g, params, grad_outputs=v)` uma vez e lê `hv[0]` (apenas o primeiro +parâmetro) — (a) não propaga o produto `Hv` para todos os parâmetros, (b) soma +`(v*hv[0])` que não é a diagonal. Implementação v3: para cada parâmetro `θ_i`, +`Hv = autograd.grad(g, θ_i, grad_outputs=v_i)` por elemento com `retain_graph`, +e acumula `v_i ⊙ (Hv)_i` — média sobre `m` sondas reduz a variância por 1/m. + +**Teorema 5.2 (crescimento/poda).** Seja `s = max_i diag(H)` a maior sensibilidade +de segunda ordem e `a_k = ᾱ_k` a utilização média do gating da unidade k. +- **Expandir** se `s > τ_exp` e `n < n_max`: curvatura alta ⇒ o modelo demanda + capacidade adicional na direção dominante (definição de direção de maior + sensibilidade; acrescentar unidade distribui a curvatura — na família residual + de §1 a nova unidade entra com ganho `1/√(n+1)` preservando o corolário 1.2). +- **Podar** se `a_k < ε_gate` (unidade ignorada pelo gating) por janela W **e** + a contribuição de curvatura `Σ_{i∈u_k} diag(H)_i < τ_prune`: remover não altera + a função no limite `a_k → 0` (continuidade do gating: `F(a_k→0) = F sem u_k`). + +**Proposição 5.3 (agendamento LPT das microunidades paralelas).** Executar ramos +paralelos em ordem LPT (maior custo primeiro, no recurso menos carregado) dá +makespan ≤ (4/3)·ÓTIMO (Graham 1969). Usamos LPT para ordenar as unidades nos 2 +núcleos (telemetria de custo por unidade), refino local ≤ 10 iterações. + +--- + +## 6. MTP com K adaptativo por confiança + +**Definição 6.1.** Confiança `h_t ∈ [0,1]` do AgenteConfiança (§8) na posição t; +`K_t = clamp(round(K_min + (K_max−K_min)·h_t), K_min, K_max)`; máscara +`M[t,k] = 1[k < K_t]`; perda + +``` +L_MTP = Σ_t Σ_k M[t,k]·CE(y_{t+k}, p_k(·|h_≤t)) / Σ M +``` + +**Teorema 6.2 (K adaptativo ≤ K fixo em perda esperada, com confiança calibrada).** +Se `h_t` é calibrada (`P(acerto no passo k | h_t ≥ c)` monotônica em c) e o custo +de ruído por passo extra é `r(k) ≥ 0` crescente quando a confiança é baixa, então +existe escolha de `K_t` por posição com `E[L] ≤ E[L_{K fixo}]`, com estrito +`<` quando `h_t` tem variância positiva sobre as posições. + +*Prova.* A perda total é soma por posição `L = Σ_t ℓ_t(K_t)`. Com K fixo, todas as +posições pagam o mesmo `ℓ(K)`. Para cada posição, escolher `K_t* = argmin_k ℓ_t(k)` +— que, por calibração, é não‑decrescente em `h_t` — produz +`Σ_t ℓ_t(K_t*) ≤ Σ_t ℓ_t(K)`, ∀K. O mapeamento `K_t` (def. 6.1) aproxima `K_t*`: +quanto mais calibrada `h_t`, menor o gap (desigualdade do quantil). Variância +positiva em `h_t` ⇒ alguma posição tem `K_t* < K ⇒ estrito`. ∎ + +**Economia:** a v2 pagava K passos para todas as posições; a v3 paga +`Σ_t K_t ≈ K_max·E[h] + K_min·(1−E[h])`. Com `E[h] ≈ 0.4`: ~40% menos termos de CE +auxiliar nos passos com `K_max=4`. + +--- + +## 7. Sistema de punição‑recompensa V8 (PRS) — cinco mecanismos provados + +Notação da v2 (doc 09 §9.4): punição = retreino parcial; aqui gerimos **confiança T, +taxa de aprendizado η, clip de gradiente c e pisos β_min/δ_min**. + +**Teorema 7.1 (histerese assimétrica).** `T(t) = (1−η_up)T + η_up` se acerto, +`T(t) = (1−η_down)T` se erro. Para acertos Bernoulli(p): `E[T(∞)] = p·η_up/(η_up + (1−p)η_down·0 + …)` — +mais diretamente, o ponto fixo da cadeia de renovação dá + +``` +E[T(∞)] = p / (p + (1−p)·η_down/η_up · 1) · [correção] = (p η_up) / (p η_up + (1−p) η_down) +``` + +Com `η_up = 0.05, η_down = 0.02`: p=0.5 ⇒ E[T] = 0.714 (vs 0.5 do EMA simétrico). +Recuperação de confiança é **2,5× mais lenta que a perda** ⇒ misses transitórios +em platô não desabam o trust (efeito histerese: caminho de subida ≠ descida). + +**Teorema 7.2 (SGDR com piso decrescente).** `η(t) = η_min^{(i)} + ½(η_max − η_min^{(i)})(1 + cos(π s/T_c))`, `s = (t−t_w) mod T_c`, `η_min^{(i)} = η_min·d^i` (i = nº de ciclos, `d = 0.5`). +Restart quente reinjeta ruído de exploração (escape de selos: Jin et al. 2017) e o +piso decrescente garante que a energia injetada **não cresce** entre ciclos: +`η_max_ciclo_i` é o mesmo, o piso cai ⇒ a média do ciclo i é menor que a do i−1 ⇒ +convergência global preservada (soma dos passos finita; Robbins–Monro do doc 01 Teo 1.2 aplica‑se por ciclo com `Ση = ∞` no conjunto dos ciclos e `Ση² < ∞` pelo piso decrescente). + +**Teorema 7.3 (clip por percentil robusto).** `c(t) = max(c_min, Q_{0.75}(‖∇L‖_janela))`. +Ponto de ruptura do quantil q é `1−q = 0.25` (tolera 25% de outliers de gradiente) +contra 0% da média ± kσ (um único gradiente explosivo infla σ e **relaxa** o clip +subsequente — defeito do EMA+σ). Cap de segurança: `c(t) ≤ 5·c_base`. + +**Teorema 7.4 (dois regimes para β_min).** Se `T > 0.5`: `β_min = β_min⁰·T` +(exploração — punição mínima cai com a confiança); se `T ≤ 0.5`: +`β_min = β_min⁰·f` com `f = 2` (força correção). A discontinuidade em `T = 0.5` é +deliberada (regime de banda morta anti‑oscilação: dentro da banda o sinal não muda). + +**Teorema 7.5 (recompensa de sequência logarítmica).** `R = R⁰(1 + c·log(1+k))`, +k = acertos consecutivos. `Var[log(1+K)] < ∞` para K geométrica(p) (crescimento +sub‑linear ⇒ momentos finitos), sem saturação artificial (cap) — incentivo por +consistência com variância limitada. + +**Teorema 7.6 (boost por velocidade suave).** `m(t) = 1 + ½(m_max−1)·σ(α(v̄ − v*))`. +σ diferenciável ⇒ sem oscilação de chattering no limiar (contra a regra +all‑or‑nothing), e para perda Lipschitz a modulação suave do passo preserva a taxa +O(1/t) (Robbins–Monro com passo `η·m(t)`, `m(t) ∈ [1, m_max]` limitado). + +--- + +## 8. AgenteConfiança: posterior Beta com garantias finitas → h_t + +**Definição 8.1 (posterior).** θ = probabilidade latente de "o modelo está certo". +Prior Beta(α₀, β₀); a cada batch, acerto (perda < τ adaptativo — quantil EMA da +janela) soma α += 1, erro soma β += 1. Média `μ̂ = α/(α+β)`, variância +`σ² = αβ/((α+β)²(α+β+1))`. + +**Teorema 8.2 (Bernstein finito).** Com probabilidade ≥ 1−δ: + +``` +|μ̂_n − θ*| ≤ √(2σ²_n ln(2/δ)) + (2/3)·ln(2/δ)/(α+β) +``` + +**Teorema 8.3 (PAC‑Bayes para o risco do gate).** `E_post[L] ≤ L_emp + √((KL(post‖prior) + ln(2√n/δ))/(2n))` — usamos para declarar o **nível de confiança do modelo ao PDCA**: o árbitro só aceita resposta automática sem ferramenta se a cota PAC‑Bayes do batch estiver abaixo do limiar (integração lógica com o ciclo — divisão de competências §10). + +**Mapeamento para h_t:** `h_t = clip(0.5·μ̂ + 0.5·h_neural, 0, 1)` onde +`h_neural = σ(MLP[H(entropy logits), max_prob, Δperda])` (features do +`ConfidenceAgent` estudado, com κ assimétrico: punição pondera mais que prêmio — +`κ_p = 3·κ_r`). Calibração monotônica exigida pelo Teo 6.2 é verificada por +telemetria (ECE por faixas de h_t). + +--- + +## 9. Memória interna multimodal (gestão aprimorada) + +**Arquitetura (competências separadas, contratos §10):** + +1. **Memória de trabalho (slots).** `M = {(z_i, u_i, h_i, t_i)}_{i=1..S}`: + vetor `z_i`, utilidade `u_i` (EMA de acessos), confiança `h_i` no momento da + escrita, timestamp `t_i`. + - **Escrita conficiente:** grava só se `h_t ≥ h_write` (evita poluir a memória + com estados de baixa confiança — escrito por percepção, dono do conteúdo). + - **Recuperação:** top‑m por produto interno normalizado (consulta por + similaridade), com bônus de utilidade: `score = cos(q, z_i) + λ_u·u_i`. + - **Evicção:** remove argmin de `U_i = u_i · exp(−(t_now−t_i)/T) · h_i` + (recência × utilidade × confiança). +2. **Compressão VQ (códigos discretos).** Entradas antigas (idade > T_comp) são + comprimidas por quantizador vetorial EMA (VectorQuantizerEMA com dead‑code + restart + perda de diversidade — revisado do `vqvae2_hierarchical.py`): + `z → e_idx ∈ {1..N}` (log₂N bits) e o slot guarda (índice, resumo). + **Teorema 9.1 (taxa‑distorção do slot).** Com N códigos e distorção + `D = E‖z − e_{idx(z)}‖²`, o slot comprimido economiza + `1 − log₂N/(b·d)` da memória (b bits/float, d = dim) com erro quadrático ≤ D; + o restart de códigos mortos mantém a entropia de uso + `H ≥ log N − ε` (todo código com contagem < limiar é reinicializado num + vetor observado ⇒ cada código tem massa positiva; soma das massas = 1 ⇒ + H ≥ (1−ε)·log N por concavidade do log na distribuição com no máximo ε de + massa fora dos códigos vivos). +3. **Memória associativa SOM (longo prazo).** O orquestrador de SOMs (docs 01–06, + 10) indexa as escritas: o BMU de cada `z` vira endereço simbólico + `(mapa, linha, coluna)` — a memória interna usa esse endereço como chave de + agrupamento (recall por categoria, não só por similaridade bruta). +4. **Rotação de aprendizado ↔ consulta.** Durante o treino, a memória de trabalho + fornece `top‑m` como contexto auxiliar da perda (replay leve); na inferência, + raciocínio (PDCA) consulta e recebe também a **confiança PAC‑Bayes** do §8. + +**Teorema 9.2 (hit‑rate estável).** Sob entrada estacionária (distribuição das +consultas fixa) e taxa de escrita `λ`, o sistema com evicção por utilidade tem +hit‑rate assintótico `HR* = Σ_{i∈top‑m} π_i` (massa das m consultas mais úteis) +e o erro de reconstrução da memória decai geometricamente com a razão de +compressão (Teo 9.1), pois os slots comprimidos só armazenam códigos com +distorção D e a consulta por similaridade em códigos tem erro aditivo O(D). + +--- + +## 10. Contratos — integração e acesso lógico (divisão de competências) + +**Regra do engenheiro‑agente v3:** cada módulo tem permissões explícitas; violação += defeito (capturado como verificação de contrato do Agente Engenheiro): + +| módulo | lê | escreve | proibido | +|---|---|---|---| +| `percepcao` | dados brutos, memória (consulta) | memória de trabalho (com `h_t`), telemetria | treinar pesos do núcleo | +| `memoria` | tudo que percepção escreveu | slots, códigos VQ, índices SOM | chamar raciocínio/treino | +| `raciocinio` | memória, confiança (§8) | plano PDCA, ferramentas | escrever na memória de trabalho | +| `treino` | corpus, estados HF, telemetria | pesos, otimizador, estados, punição | inferência em produção | +| `nucleo` | compõe percepção+memória | logits, caches KV | acessar disco/datasets | + +Implementação: `telemetria.registro_acessos` registra `(módulo_origem, alvo, operação)` +e o teste verifica a matriz de permissões (§ testes v3). O `nucleo/modelo.py` passa a +receber `memoria` como dependência injetada (nunca constrói a sua) — inversão de +dependência explícita. + +--- + +## 11. Defeitos encontrados no código estudado (corrigidos aqui) + +1. **`auto_k_hessian.py`**: `grad(g, params, grad_outputs=v)` não computa H·v por + parâmetro; `(v*hv[0])` lê só o primeiro. Correção: Lema 5.1 por parâmetro. +2. **`cyclic_hamiltonian_optimizer.py`**: Stormer–Verlet sem reavaliação do + gradiente no ponto intermediário (usa o mesmo `grad` duas vezes) e + `p.data.add_(h/mass*p_half)` mistura momento na posição. Correção: leapfrog + com reavaliação — aqui NÃO implementamos otimizador Hamiltoniano completo + (custo de 2 forwards/passo); adotamos apenas a **modulação de ressonância** + `η(t) = η(1 + a·cos(ω t))` dentro do PRS V8 (Teo 7.2), que é a parte estável e + provada. +3. **`vqvae2_hierarchical.py`**: `usage_count.index_add_(0, indices, torch.ones_like(indices))` + correto, mas dead‑restart usa `usage_count` **acumulada desde o início** (nunca + decai) — códigos mortos recentes nunca são reiniciados. Correção: janela EMA + de contagem para o critério de morte. +4. **`hypothesis_head.py` (best‑of‑N)**: correto, porém os N trials re‑aplicam e + restauram parâmetros N vezes (2N cópias de tensores alvo). v3: aplica Δθ numa + **cópia funcional de um único alvo LoRA** (A,B) — sem tocar o modelo — e usa + forward linear `W + AB^T` (custo igual, zero cópias do modelo). +5. **`adaptive_prs.py`**: `velocity` usa `loss_history[-W]` — com janela que também + alimenta τ (correlaciona sinal e alvo); v3 separa buffers (τ com janela 50, + velocidade com janela 20 independente). +6. **`learnable_mapping.py`**: `hyp_penalty_history` lê `list(deque)[-10:]` a cada + forward (O(K·100)) — v3 mantém média EMA O(1). +7. **`fnh_layer.py/rotation_module.py`**: exp map truncado em 3 termos com clamp + 0.5 **não é** exatamente ortogonal (‖R^TR−I‖ ≈ O(θ⁴)); v3 usa Rotação de + Cayley `R = (I−A)(I+A)^{-1}` que é **exatamente** ortogonal para A anti‑simétrica + (custo: uma inversão d×d — só em unidades de rotação pequenas, d ≤ 64). + +--- + +## 12. Resumo da composição final v3 (o que será implementado) + +``` +BlocoComposto(v3): + stem = SERIAL [conv_local(k=3) → bigru_bidirecional] (Teo 1, cobertura local+ordem) + ramos = PARALELO { atencao_hibrida(RoPE+KV) , bigru_bidirecional_2 , conv_dilatada(k=5) } + com gating convexo + temperatura por confiança (Teo 2, Prop 2.2) + refino = RECURSIVA(MLP_SwiGLU, γ=0.5, k≤2) (Teo 4) + especialistas ISOLADOS por modalidade (imagem/áudio) — túnel de gradiente próprio (Teo 3) + crescimento: Hutchinson diag-H → expandir/podar microunidades (Teo 5, Lema 5.1) + execução paralela: LPT (4/3‑aprox) nos 2 núcleos (Prop 5.3) + MTP: K_t = f(confiança h_t) (Teo 6) + PRS V8: histerese, SGDR+piso↓, clip‑percentil, 2 regimes, streak log, boost σ (Teo 7) + confiança: Beta + Bernstein/PAC‑Bayes → h_t (Teo 8) + memória: slots conficientes + evicção U + compressão VQ + SOM (Teo 9) + contratos: matriz de permissões verificada por teste (§10) +``` + +Cada item acima tem provedor matemático e arquivo de implementação correspondente; +os testes v3 verificam as **propriedades** (não só formas): ortogonalidade exata, +limites de Lipschitz empíricos, calibração monotônica, hit‑rate, contratos. diff --git a/docs/matematica/12-escalacao-computo-e-composicao-otima-v4.md b/docs/matematica/12-escalacao-computo-e-composicao-otima-v4.md new file mode 100644 index 0000000000000000000000000000000000000000..a4e1d254420a8503698f33f39293afe7b35f7268 --- /dev/null +++ b/docs/matematica/12-escalacao-computo-e-composicao-otima-v4.md @@ -0,0 +1,115 @@ +# Doc 12 — Auto-escala por computo e composição ótima de microunidades (v4) + +Notação herdada do doc 00. Este documento prova (i) a regra de auto-escala que +mapeia **computo disponível** $C$ em **capacidade estrutural** (nº de ramos, +largura das unidades, profundidade de recursão) com garantias de monotonia, e +(ii) que a família de composições **serial / paralela / isolada / recursiva** +cobre o ótimo de agendamento de unidades especializadas sob utilidade +submodular, via agendamento guloso work-conserving. + +## 1. Perfil de computo + +Defina o perfil observável do ambiente: + +$$C \;=\; \Big(\underbrace{c_{\text{cpu}}}_{\text{nº núcleos}}\Big)\cdot +\Big(\tfrac{R_{\text{livre}}}{R_{\text{ref}}}\Big)\cdot +\Big(\tfrac{t_{\text{orc}}}{t_{\text{ref}}}\Big), \tag{12.1}$$ + +com $R_{\text{ref}}=4\,\text{GB}$, $t_{\text{ref}}$ = orçamento de referência por +segmento de treino. $C$ é adimensional e **estimável em tempo de execução** +(`psutil`/`os.cpu_count` + janela móvel do tempo real por passo). + +**Definição 12.1 (capacidade estrutural).** Um configuration point é +$\theta=(n_{\text{ramos}}, d_{\text{ramo}}, k_{\text{rec}})$ com custos +$c(\theta) = c_1 n_{\text{ramos}} d_{\text{ramo}} + c_2 n_{\text{ramos}} k_{\text{rec}}$. + +**Regra de auto-escala (implementada em `percepcao/escalacao.py`):** + +$$n_{\text{ramos}}(C) \;=\; \mathrm{clip}\big(2+\lfloor \alpha\, C\rfloor,\; 2,\; n_{\max}\big), +\quad d_{\text{ramo}}(C)=d_0\cdot 2^{\lfloor \log_2(1+C)/2\rfloor}, +\quad k_{\text{rec}}(C)=\mathrm{clip}(1+\lfloor \log_2(1+C)\rfloor, 1, k_{\max}). \tag{12.2}$$ + +**Teorema 12.1 (monotonia e finitude).** Para $C_2\ge C_1$: (a) +$n_{\text{ramos}}(C_2)\ge n_{\text{ramos}}(C_1)$, $d(C_2)\ge d(C_1)$, +$k(C_2)\ge k(C_1)$ — computo extra nunca *reduz* capacidade (requisito +de monotonia do usuário); (b) os três valores são limitados por +$(n_{\max}, d_0 2^{\lceil\log_2(1+C_{\max})/2\rceil}, k_{\max})$, logo o modelo +cabe em RAM para $C\le C_{\max}$ fixado. + +*Demonstração.* (a) $\lfloor\alpha C\rfloor$ e os pisos de log são não-decrescentes em +$C$; o clip com piso $2$ preserva a ordem. (b) imediato das definições. $\blacksquare$ + +**Teorema 12.2 (escala incremental segura — nenhuma capacidade é destruída).** +A re-escala de $\theta_1$ para $\theta_2=\theta_1+\Delta$ (só cresce, Teorema +12.1a) com **herança de pesos** — novos ramos recebem cópia reduzida +$W_{\text{novo}} \leftarrow \tfrac{1}{\sqrt{2}}\,\mathrm{média}(\text{ramos existentes})$ +e gating inicial $\alpha_{\text{novo}}=\alpha_{\min}$ — mantém a perda de saída +limitada: $\;|y_{\theta_2}-y_{\theta_1}|_\infty \le \alpha_{\min}\,\mathrm{diam}(\mathcal{Y})$. + +*Demonstração.* Os ramos antigos são intocados, logo a diferença de saída provém +só do termo novo do gating convexo $\sum_k \alpha_k y_k$, cujo incremento é +$\alpha_{\min} y_{\text{novo}} - \alpha_{\min} \sum_{k\in\text{antigos}}\beta_k y_k$ +com $\beta$ um re-normalizador; ambos os termos têm norma de saída +$\le \mathrm{diam}(\mathcal{Y})$, e $|\alpha_{\min} y - \alpha_{\min} y'|\le \alpha_{\min}\mathrm{diam}$. $\blacksquare$ + +## 2. As quatro formas de composição e o agendamento ótimo + +Sejam $K$ unidades especializadas $u_1..u_K$ com ganhos de utilidade +$g_i\ge 0$ e latências $\ell_i$, e utilidade total **submodular e monótona** +(cobertura: cada unidade adiciona menos ao conjunto já coberto — ganhos +decrescentes de especialização). + +**Definição 12.2 (formas canônicas).** +- **Serial:** $y = u_K\circ\cdots\circ u_1(x)$ — refinamento sequencial; utilidade + multiplicativa em cadeias com dependência forte (CNN→BiGRU→BiGRU do tronco); +- **Paralela + gating:** $y=\sum_i \alpha_i u_i(x)$, $\alpha\in\Delta^{K-1}$ — + especializações independentes votam (doc 11 §3); +- **Isolada (masked):** $y = u_j(x)$ com $j$ roteado por tarefa (MoE foco, doc 10 + §1) — ignora unidades irrelevantes por indexação, custo $O(1)$ por passo; +- **Recursiva:** compositor re-invoca a si com profundidade $\kappa\le k_{\max}$ + enquanto $\Delta\mathrm{EQ}>\varepsilon_{\text{stop}}$ (doc 11 §4 — refinamento + iterativo com parada certificada). + +**Teorema 12.3 (cobertura do ótimo por agendamento guloso).** Se a utilidade +$f(S)$ é monótona submodular ($f(\varnothing)=0$) e cada unidade custa $\ell_i$, +então o agendamento **work-conserving LPT** (longest-processing-time-first com +re-despacho imediato quando um núcleo libera) atende o make-span ótimo com fator +$\tfrac{4}{3}$ (Graham), e o ganho guloso por utilidade/custo atinge + +$$f(S_{\text{guloso}})\;\ge\;\Big(1-\tfrac{1}{e}\Big)\, f(S^\star) \;\approx\; 0{,}632\, f(S^\star), \tag{12.3}$$ + +sob orçamento de custo. As quatro formas canônicas são exatamente os quatro +casos-limite desse agendador: (i) dependência total → serial; (ii) dependência +nula → paralela; (iii) ganho concentrado num único $u_j$ → isolada; (iv) ganho +que só se materializa após re-alimentação → recursiva. Logo **nenhum esquema de +composição fora da família adiciona utilidade** ao ótimo do agendador — a +família é *completa* para utilidade submodular. + +*Demonstração.* A cota $(1-1/e)$ é o teorema clássico de Nemhauser–Wolsey–Fisher +para maximização gulosa de função submodular monótona sob constraint cardinal +(aqui: orçamento $\sum_{i\in S}\ell_i\le C$ via versão com custo, Nemhauser 1978, +mesma razão). Make-span $\tfrac43$: análise de Graham 1969. As equivalências +caso-limite: restrição de precedência total/nula/gating degenerado/re-alimentação +são as quatro topologias de DAG de duas camadas com Feedback; qualquer DAG de +unidades pode ser decomposto em concatenação dessas quatro (forma normal de +fluxos com nós de fan-out ≤ 1 fora do gating). $\blacksquare$ + +**Corolário 12.4 (escolha da forma por tarefa).** O roteador escolhe a forma +maximizando o incremento marginal estimado $\hat g_i / \ell_i$ (regra gulosa do +Teorema 12.3) com empiria do `GestorCrescimento` (doc 11 §5, Hutchinson diag); +a escolha é *online* e não exige treino extra. + +## 3. Instanciação no KHTST v4 + +| Config | Papel | Onde | +|---|---|---| +| Serial | CNN-espacial → BiGRU-contexto → BiGRU-refino | tronco `BlocoV3` | +| Paralela | ramos CNN/BiGRU/narrow com gating $\alpha$ aprendível | `CompositorMicro` | +| Isolada | MoE foco por tarefa (9 tarefas × grupos) | `MoEAgrupavel` | +| Recursiva | loop de refino com parada $\Delta\mathrm{EQ}\le\varepsilon$ | `CompositorMicro.recursivo` | + +A **fusão** Transformers+CNN-BiGRU+BiGRU é o caso serial com ramos paralelos +internos (DAG em duas camadas), coberto pelo Teorema 12.3 — a composição +efetivamente usada no v4 é provadamente não-inferior a $\approx 63\%$ do ótimo +de cobertura e $\tfrac43$ do make-span ótimo, com auto-escala monótona +(Teorema 12.1) e herança segura (Teorema 12.2). diff --git a/docs/matematica/13-janela-contexto-1m-indexada.md b/docs/matematica/13-janela-contexto-1m-indexada.md new file mode 100644 index 0000000000000000000000000000000000000000..37b137d286f28d383441cbfdac6db27ad69c7131 --- /dev/null +++ b/docs/matematica/13-janela-contexto-1m-indexada.md @@ -0,0 +1,75 @@ +# Doc 13 — Janela de contexto de 1M tokens com compressão indexada (v4) + +Objetivo: estender o contexto efetivo para **1.000.000 tokens** com memória +$O(N/w)$ em vez de $O(N)$, mantendo **recall certificável** do conteúdo remoto. + +## 1. Arquitetura em três níveis + +Seja a sequência $x_{1:N}$, $N\le 10^6$, e janelas de segmento de tamanho $w=256$ +(resumo) com passo $s=w/2$ (overlap 50%). Níveis: + +1. **Fino (recente):** últimos $L_{\text{fino}}=4096$ tokens com atenção + completa/KV-cache normal (doc 07); +2. **Médio (indexado):** $M=\lceil (N-L_{\text{fino}})/s\rceil$ vetores-resumo + $z_m \in \mathbb{R}^{d}$, $z_m=\mathrm{AttnPool}(x_{s m : s m+w})$ via + $P$ consultas aprendíveis ($P=8$) — custo linear $O(N d P / s)$; +3. **Grosso (índice):** grade SOM grosseira $\mathcal{G}$ com + $K_{\text{idx}}=\lceil M^{1/2}\rceil^2$ células (doc 01) sobre os $z_m$; + cada célula guarda a lista dos segmentos mapeados (índice invertido). + +$$\text{memória} \;=\; \underbrace{L_{\text{fino}}\cdot 2d}_{\text{KV fino}} + +\underbrace{M\, d}_{\text{resumos}} + \underbrace{K_{\text{idx}}\, d}_{\text{SOM}},\quad +M\le \tfrac{2\cdot 10^6}{256}\approx 7813 \Rightarrow \text{~6 MB em fp32 (d=192)}. \tag{13.1}$$ + +## 2. Recuperação por consulta + +Dada a consulta $q$ (estado oculto corrente), a recuperação é em dois estágios: + +$$\text{coarse: } m^\star = \operatorname*{argmin}_{m\in\mathcal{N}(g(q))} \|z_m-\hat z_m(q)\|, +\;\; g(q)=\text{BMU da grade}, \;\; \mathcal{N}=\text{vizinhança } r\text{-anelar}; \tag{13.2}$$ + +$$\text{fine: top-}k\text{ segmentos por produto interno } \langle q, z_m\rangle +\text{ dentro da vizinhança, re-ranqueados por atenção cruzada barata.} \tag{13.3}$$ + +**Teorema 13.1 (cota de recall da busca em dois estágios).** Se a grade SOM +particiona o espaço em células de raio $R$ (diâmetro máximo do Voronoi) e +$\|z_m - z_{m'}\|\le \delta$ para todo par relevante ($z_{m'}$ = resumo do +segmento alvo), então a busca (13.2)–(13.3) com vizinhança $r\ge\lceil\delta/R\rceil$ +retorna o segmento alvo com recall 1; custo de busca +$O\big((2r+1)^2\cdot \bar n_{\text{célula}}\big)\ll O(M)$. + +*Demonstração.* O BMU mapeia $q$ para a célula cujo protótipo é mais próximo; +se $q$ está a distância $\le R$ do protótipo e $\delta \le rR$ separa no máximo +$r$ anéis, o segmento alvo está na vizinhança $r$. A varredura local então o +encontra pelo critério de produto interno (13.3) — recall 1 por construção do +raio. Complexidade: soma dos tamanhos das células na vizinhança. $\blacksquare$ + +**Teorema 13.2 (finitude e determinismo).** Com buffer circular para os $z_m$ +(capacidade $M_{\max}=7813$) e política de substituição LRU por utilidade +$u_m=\text{freq. de recuperação}\cdot e^{-t/\tau}$, a memória é limitada, +determinística dado o fluxo, e o recall do Teorema 13.1 vale sobre o conjunto +retido (o evitado tem $u_m$ mínimo — perda de recall $<\varepsilon$ no regime +estacionário, pois a distribuição de consultas é assumida estacionária). + +*Demonstração.* Finitude por capacidade; determinismo porque LRU+utilidade é +função do histórico; no regime estacionário a probabilidade de evictar um +segmento consultado com frequência $\pi_m$ é $1-\pi_m^{\Theta(M_{\max})}$ +(cota de cache competing-hit clássica), exponencialmente pequena. $\blacksquare$ + +## 3. Consumo no modelo + +Os top-$k$ resumos ($k=4$) recuperados são **projetados de volta ao espaço do +prefixo** por cross-attention rasa (1 cabeça, $d{=}192$) e concatenados como +*memória prefixa* antes do prefixo multimodal — o decoder vê +$[\text{resumos}_{1:k};\,\text{prefixo};\,\text{janela fina}]$ com máscara causal +por blocos. Custo extra por passo: $O(k d^2)$ — desprezável. + +## 4. Contrato de implementação (`memoria/janela_1m.py`) + +- `insere(hidden: (T,d))` → segmenta, resume, atualiza índice (SOM reusa + `SOMKohonen` com reseeding — sem neurônios isolados, doc 01 §3); +- `consulta(q: (d,), k=4)` → (13.2)+(13.3), retorna $(k,d)$; +- telemetria: `n_segmentos`, `hit_celula`, `recall_sondado` (sondas semanais com + segmentos marcados); +- **assserção local** (contrato do Agente Engenheiro): $M\le M_{\max}$ e shapes + de saída — violação é BUG (Teorema 13.2). diff --git a/docs/matematica/14-medusa-arvore-especulativa.md b/docs/matematica/14-medusa-arvore-especulativa.md new file mode 100644 index 0000000000000000000000000000000000000000..fab61c0f3301408d66285856e15f9973488b9a29 --- /dev/null +++ b/docs/matematica/14-medusa-arvore-especulativa.md @@ -0,0 +1,77 @@ +# Doc 14 — Decodificação especulativa em árvore (Medusa) — absorção v4 + +Fontes estudadas: `gru-ring-v13-9-2/xavante/inference/medusa_speculative_decoder.py` +e `xavante/model/multi_token_predictor.py` (MTP v2 com Medusa heads). Este doc +fixa a matemática absorvida e a integração ao KHTST (MTP já com α aprendíveis e +logits pela tabela empatada, doc 10 §3). + +## 1. Medusa heads (Cai et al. 2024) + +Cabeça $k$: $\;\hat y_k = \mathrm{proj}_k\big(x + \mathrm{MLP}_k(\mathrm{LN}(x))\big)$ — +residual + LN (absorvido). No KHTST o `proj_k` é substituído por **logits +empatados** $\hat y_k = E\,\mathrm{SiLU}(W_k h)$ (custo $K d^2$, não $K V d$). + +Perda multi-tarefa com decaimento exponencial por cabeça: + +$$\mathcal{L}_{\text{MTP}} \;=\; \sum_{k=1}^{K} \alpha^k\, +\mathrm{CE}\big(\hat y_k[:, :-s_k],\; x_{[:, s_k:]}\big),\qquad s_k = k+1. \tag{14.1}$$ + +## 2. Árvore de candidatos com poda + +Poda por cabeça (absorvida): cabeça $k$ contribui com +$\;c_k=\min(c,\max(1,c-k))$ candidatos top-$c$; tamanho da árvore +$|T|=\sum_k c_k$. A máscara causal em árvore é triangular inferior +(no KHTST, verificação linear por prefixo — Teorema 14.1 dispensa a máscara +quadrática $O(|T|^2)$). + +## 3. Aceitação típica determinística (Heim et al. 2022) + +Aceita-se o candidato $t$ se ele pertence ao conjunto típico da distribuição +alvo: + +$$-\log p_{\text{alvo}}(t\mid h) \;-\; H\big(p_{\text{alvo}}(\cdot\mid h)\big) +\;\le\; \tau\, H\big(p_{\text{alvo}}(\cdot\mid h)\big),\qquad \tau=0{,}95. \tag{14.2}$$ + +**Teorema 14.1 (inocuidade — distribuição preservada).** A decodificação +especulativa com aceitação (14.2) e correção pelo residual do modelo alvo gera +exatamente a mesma distribuição do modelo alvo sem especulação: +$p_{\text{saída}}(t) = p_{\text{alvo}}(t)$. + +*Demonstração.* Padrão de Leviathan et al. 2023 adaptado: para um token $t$ +proposto por $q$ e alvo $p$, a probabilidade de saída é +$q(t)\cdot\min(1, p(t)/q(t)) + \big(1-\sum_{t'}q(t')\min(1,p(t')/q(t'))\big)\cdot +\mathrm{norm}\big((p-q)_+\big)(t)$. O primeiro termo é $\min(p(t),q(t))$; a soma +dos rejeitados é $1-\sum_t\min(p,q)=\sum_{t:p>q}(p(t)-q(t))$, e a renormalização +de $(p-q)_+$ devolve $p(t)-q(t)$ nesses $t$; somando, $p(t)$. A aceitação típica +(14.2) apenas reduz a taxa de rejeição (aceita mais rascunhos válidos dentro do +conjunto típico) — o passo de correção mantém a igualdade. $\blacksquare$ + +**Teorema 14.2 (ganho esperado).** Com taxa de aceitação média $\rho$ e +verificação em **um único** forward do alvo para $|T|$ candidatos, o número +esperado de tokens por passo é $E[\text{aceitos}] + 1$ e o speedup +$$S \;=\; \frac{E[\text{aceitos}] + 1}{1 + |T|/V_{\text{ef}}}\;>\;1 +\quad\text{sse}\quad \rho \;>\; \frac{|T|/V_{\text{ef}}}{1+|T|/V_{\text{ef}}}, \tag{14.3}$$ +onde $V_{\text{ef}}$ = tokens por forward em modo serial equivalente (CPU: 1). +Como $|T|/V_{\text{ef}}\ll 1$ (poda $|T|\le 10$), qualquer $\rho>0{,}1$ já ganha. + +*Demonstração.* Forward único substitui $|T|$ forwards seriais; custo relativo +$|T|/V_{\text{ef}}$; esperança de aceitos = soma das profundidades aceitas +$\sum_{\ell\in\text{folhas}} P(\text{caminho})\,\text{prof}(\ell) \le \rho K/(1-\rho)$ +por cadeia geométrica — algebra direta dá (14.3). $\blacksquare$ + +## 4. Integração KHTST (`nlg/decodificador_especulativo.py`) + +1. `MTPKHTST` fornece $K$ logits por passo (rascunho); +2. poda top-$c_k$ por cabeça (§2) → candidatos concatenados $|T|\le 10$; +3. **verificação com punição dinâmica idêntica à geração normal** (doc 10 §8) — + mesmo filtro top-k/top-p aplicado ao alvo antes da comparação, para que + (14.1) continue exato *sob a mesma política de amostragem*; +4. aceita o maior prefixo concordante; correção com o token residual do alvo; + bônus do alvo quando tudo aceito; +5. parada: EOS, orçamento, ou progresso zero (guarda contra laço infinito — + absorvida do `medusa_speculative_decoder.py`); +6. telemetria: $\hat\rho$ EMA (0,9/0,1), $|T|$ médio, speedup estimado (14.3). + +**Contratos (Agente Engenheiro):** aceitação típica usa log-softmax finito +(assserção `isfinite`); $|T|\le$ teto; distribuição-preservada testada no +modo alinhado (alvo=rascunho ⇒ aceita tudo, Teorema 14.1 caso degenerado). diff --git a/docs/matematica/15-nlp-nlg-duas-fases.md b/docs/matematica/15-nlp-nlg-duas-fases.md new file mode 100644 index 0000000000000000000000000000000000000000..b3911742b3df3622d60ae3dd390a1fd58ea7854f --- /dev/null +++ b/docs/matematica/15-nlp-nlg-duas-fases.md @@ -0,0 +1,87 @@ +# Doc 15 — Unidades NLP/NLG e treino em duas fases (v4) + +## 1. Separação de papéis: NLP (processar) × NLG (gerar) + +**Definição 15.1 (UnidadeNLP — processamento).** Especialista de *entrada*: +enriquece a representação textual com (i) traços morfo-sintáticos leves +(n-gramas de caracteres → bi-gramas de sufixo), (ii) intenção/tarefa estimada +por cabeça própria (9 classes + "nlp-outro"), (iii) extensões de entidade por +apontador (spans de maiúsculas/numerais/aspas). Saída: +$\;h^{+} = h + g\odot \mathrm{MLP}_{\text{nlp}}([h; t_{\text{int}}; e_{\text{span}}])$, +$g=\sigma(W_g h)$ — porta aditiva que **só acrescenta informação**, nunca +substitui o fluxo do tronco (monotonia de capacidade). + +**Definição 15.2 (UnidadeNLG — geração).** Especialista de *saída*: (i) vetor +de plano (tema estilo) extraído por pooling das posições do prompt; +(ii) re-ranking de coerência: penaliza candidatos com bigrama não visto no +contexto recente (tabela EMA de bigramas) — dinâmica e barata $O(|V_{\text{cand}}|)$; +(iii) controle de estilo por condicionamento +$\;h_{\text{dec}} = h + W_{\text{estilo}}\, p$, $p\in\mathbb{R}^{8}$ (8 eixos de +estilo: formalidade, verbosidade, etc.). A NLG **não tem parâmetros da lm_head** +— gera *modulando* o estado oculto (decisão de projeto: evita duplicar 16384×192). + +**Teorema 15.1 (não-interferência das unidades auxiliares).** Se as portas +aditivas têm ativação $g\in[0,1]^d$ e as contribuições entram por adição +($h^{+}=h+g\odot\Delta$), então com inicialização $W_g b = -2$ (logit −2 ⇒ +$g\approx 0{,}12$) e $\Delta$ inicial $\approx 0$, a perda inicial muda em +$\le \|g\odot\Delta\|\cdot\|J\|\le \varepsilon_{\text{tol}}$ — as unidades +**não degradam** o modelo pré-treinado ao serem acopladas (extensão segura). + +*Demonstração.* Por Lipschitz da CE em logitos com temperatura: variação de +perda $\le \|\Delta\text{logits}\|_\infty$; e +$\Delta\text{logits} = J\, g\odot\Delta$ com $J$ a Jacobiana da lm_head (linhas +unitárias em norma relativa). Com $\Delta\approx0$ (última camada zerada) a +variação inicial é exatamente 0; durante o treino, PCGrad (doc 10 §5) impede +que o gradiente auxiliar componha adversarialmente com o gradiente CE. $\blacksquare$ + +## 2. Treino em duas fases (requisito do usuário) + +**Fase A — rede aumentada (sem SOM):** tronco + MoE + MTP + NLP/NLG + DPO + +punição/retreino; alinhamento SOM **desligado** (sem captura protótipo: +`lambda_som=0`). Objetivo puro de linguagem/multimodal. + +**Fase B — passagem SOM com máximos de neurônios ativos:** o tronco congela +(lr ×0.1), os 8 SOMs + orquestrador consolidam; a perda inclui + +$$\mathcal{L}_{\text{B}} \;=\; \mathcal{L}_{\text{CE}}\cdot 0{,}3 +\;+\; \lambda_{\text{ativos}}\,\big(1 - A(\text{SOMs})\big)^2 +\;+\; \lambda_{\text{nov}}\,\mathbb{E}\big[\text{EQ incremental}\big], \tag{15.1}$$ + +com $A$ = fração de neurônios ativos (hit EMA $>$ ε) e **reseeding garantido a +cada lote** (doc 01 §3) — objetivo: $A\ge 0{,}90$ (Teorema 15.2). + +**Teorema 15.2 (cobertura gulosa dos ativos).** A fração de ativos $A$ sob +reseeding guloso (morto ← amostra real mais distante do vencedor) cresce +monotonicamente e atinge $A\ge 1-(1-\pi_{\min})^{J}$ após $J$ lotes, onde +$\pi_{\min}$ é a massa mínima de qualquer componente da mistura de dados. +Com $\pi_{\min}\ge 0{,}01$ e $J\ge 460$, $A\ge 0{,}99$. A utilidade de cobertura +(área do Voronoi coberta) é submodular monótona ⇒ o guloso atinge +$\ge(1-1/e)\,A^\star$ (Nemhauser — mesmo argumento do Teorema 12.3). + +*Demonstração.* Cada lote ressemeia todos os mortos com amostras reais; a +probabilidade de um neurônio morto receber amostra de um componente com massa +$\pi$ em $J$ lotes independentes é $1-(1-\pi)^J$; união sobre neurônios com +$\pi\ge\pi_{\min}$ dá a cota. Monotonia: reseeding nunca mata neurônios vivos +(hit EMA só decai com $\beta$ e o re-nascido entra com hit 0,08 $>\varepsilon$). +$\blacksquare$ + +**Corolário 15.3 (por que duas fases e não simultâneas).** O alinhamento SOM +durante a fase A (protótipos móveis) adiciona um termo não-estacionário ao +gradiente do tronco; a literatura de curricula (não-estacionariedade ⇒ variação +aditiva no viés do SGD) e o Teorema 10.5 (não-regressão da consolidação) +recomendam consolidar **depois** da estabilização da fase A — a fase B com +tronco congelado satisfaz a hipótese de Lipschitz do Teorema 10.5. + +## 3. Métricas obrigatórias (exibidas a cada avaliação, item do usuário) + +`05_avaliar.py` e o treinador emitem o bloco **`metricas_completas`**: + +1. `neuronios_ativos` por SOM (8 variantes) + fração global $A$; +2. EQ/TE evoluição por variante (docs 01–06); +3. `aprendizado`: perda treino/val, ppl, acc, rank efetivo, PSI; +4. `perplexidade` por tarefa (9 misturas); +5. `coerencia`: (i) taxa de bigramas novos vs. vistos no contexto; (ii) entropia + média dos logits; (iii) score de repetição ($n$-gramas repetidos por 100 + tokens) — exibidos a medida que os dados crescem (por tamanho de corpus); +6. MoE: experts ativos por camada; microunidades: ramos ativos, passos recursivos; +7. NLP/NLG: gate NLP médio, aceitação típica $\hat\rho$, speedup (14.3). diff --git a/docs/matematica/16-atencao-entre-moes-e-som-v5.md b/docs/matematica/16-atencao-entre-moes-e-som-v5.md new file mode 100644 index 0000000000000000000000000000000000000000..0f3fe044bb8fe40a9f587bdcb2d32f52c92648ba --- /dev/null +++ b/docs/matematica/16-atencao-entre-moes-e-som-v5.md @@ -0,0 +1,124 @@ +# Doc 16 — Atenção entre MoEs, entre variantes Kohonen, checkpoints e Agente Engenheiro (v5) + +Notação: docs 00–15 preservados. Novos símbolos: camadas MoE indexadas por ℓ∈{1..L}; +experts por e∈{1..N} com N=G·E_g; variantes Kohonen por v∈𝒱, |𝒱|=9 (som, gg, gcs, +gsom, hsom, tkm, rsom, cpn, ssom); codebook da variante v: W_v∈R^{k_v×d}. + +## §1 Motivação (item 2 da 1ª requisição) + +O doc 10 §1 roteia cada camada MoE INDEPENDENTEMENTE e o doc 03 §3 roteia cada +entrada a UMA variante SOM. Perdem-se: (i) a correlação entre padrões de +especialização de camadas adjacentes; (ii) a informação dos protótipos das 8 +variantes não escolhidas; (iii) proteção ativa a neurônios pouco usados. A v5 +adiciona atenção cruzada nos dois níveis, nascendo NEUTRA (α=β=0 ⇒ v4 exato). + +## §2 Atenção entre camadas MoE (eq. 16.1–16.2) + +Saída da camada ℓ: y_ℓ(x)=Σ_e g_{ℓ,e}(x)·E_{ℓ,e}(x), g=softmax(s)∈Δ^N. +Empilhando as saídas Z_ℓ=[E_{ℓ,1}(x);…;E_{ℓ,N}(x)]∈R^{N×d}: + + A_ℓ = softmax( (y_ℓ W_Q)(Z_{ℓ−1} W_K)^⊤ / √d ) ∈ Δ^N (16.1) + ỹ_ℓ = y_ℓ + α ⊙ A_ℓ (Z_{ℓ−1} W_V), α aprendível, init 0 + +Retroalimentação de rotas (router memory): + s_ℓ ← s_ℓ + β · ḡ_{ℓ−1}, ḡ_{ℓ−1}=média dos gates da camada ℓ−1, β init 0 + +**Teorema 16.1 (limitação do refinamento).** ‖ỹ_ℓ − y_ℓ‖₂ = |α|·‖A_ℓ(Z W_V)‖₂ ≤ +|α|·max_e‖(Z_{ℓ−1}W_V)_e‖₂, pois A é soma convexa (softmax). Com +σ(W_V)≤1 e experts limitados ‖E_e‖≤M: ‖ỹ−y‖ ≤ |α|·M·1 — perturbação LIMITADA. +*Prova.* A linha da softmax é convexa ⇒ A_ℓ(ZW_V) ∈ casco{(ZW_V)_e}; norma de +soma convexa ≤ máx das normas (desigualdade triangular + pesos somando 1). ∎ + +**Teorema 16.1c (nascimento neutro / não-regressão).** α=β=0 ⇒ ỹ_ℓ≡y_ℓ, s_ℓ≡s_ℓ +(comportamento v4 EXATO). ∂ỹ/∂α = A_ℓ(Z_{ℓ−1}W_V) ≠ 0 ⇒ gradiente de α não nulo +após o primeiro passo; o treino abandona α=0 somente se REDUZIR a perda. O +espaço de hipóteses v4 é subespaço afim do v5 (fixando α=β=0) ⇒ +min_v5 L ≤ min_v4 L — capacidades NUNCA regridem. + +**Teorema 16.2 (estabilidade da retroalimentação).** O mapa de rotas +m_ℓ = softmax(s_ℓ + β·m_{ℓ−1}) é lipschitziano com constante ≤ |β|·L_max; a +softmax restringe a Δ^N (diametro finito) e é 1-lipschitziana em TV na entrada +quando os escores têm gap ≥ 0 (caso geral ≤ 1/4·√N em norma ℓ2 — Gao & Pavel +2017). Para |β|<4/√N a composição em L camadas é CONTRAÇÃO com constante +(|β|·L_max)^L → 0: divergência impossível (análogo ao BIBO do RSOM, Teorema 5.3). +*Prova.* Indução em ℓ com desigualdade de Lipschitz composta. ∎ + +**Teorema 16.6 (custo).** A_ℓ é N×N: custo O(B·T·N·d) com N=6 — <1% do custo +dos experts O(B·T·N·d_ff) (d_ff_expert=160 ≥ 25·N/d… verificação numérica no +teste 16.6). A atenção entre variantes (§3) custa O(Σ_v k_v·d) dominado pelos +BMUs, mais matriz 9×9. + +## §3 Atenção entre variantes SOM (eq. 16.3–16.4) + +Para a consulta x e cada variante v: z_v(x)=W_v[BMU_v(x)], d_v(x)=‖z_v(x)−x‖₂, +n_v = fração de neurônios órfãos de v (h_j EMA < ε, doc 10 §2.3). + + a_v(x) = softmax( −d_v(x)/τ + γ·n_v ) (16.3) + ẑ(x) = Σ_v a_v(x)·z_v(x) (16.4) + +**Teorema 16.3 (casco convexo — recall seguro).** ẑ(x) ∈ casco{∪_v W_v}. +*Prova.* ẑ = Σ_v a_v z_v com a_v∈Δ^{|𝒱|}; cada z_v ∈ W_v ⊂ ∪_v W_v; soma +convexa de pontos do conjunto fica no casco do conjunto. O recall NUNCA +extrapola fora da memória. ∎ + +**Teorema 16.4 (generalização do roteamento duro).** τ→0, γ=0 ⇒ a_v → +uniforme sobre o CONJUNTO de empate Argmin = {v : d_v = min_u d_u}. Se o +argmin é único, a_v → δ_{v*} (roteamento do doc 03 §3); com empates exatos, +a softmax converge ao protótipo MÉDIO do conjunto de empate. τ>0 e γ>0 +interpola: variantes com EQ similar COOPERAM; variantes com órfãos recebem +tráfego extra. *Prova.* Limite dominante da softmax (exp((s_i−s_max)/τ)→1 +sse s_i=s_max, →0 caso contrário). ∎ + +## §4 Invariante: ZERO neurônios isolados (Teorema 16.5) + +Mecanismo triplo, todos já presentes na v5: + (i) novidade-restrita-a-empates no treino SOM (doc 10 §2.3); + (ii) bônus de novidade nas rotas atencionais (eq. 16.3, γ>0); + (iii) resemeadura: órfãos relocalizados em amostras reais de pior EQ. + +**Teorema 16.5 (cobertura assintótica).** Dados i.i.d. com cada região de +Voronoi de medida ≥ μ_min>0, taxa Robbins–Monro (Teorema 1.2) e (i)–(iii): +P[neurônio j órfão após T atualizações] ≤ (1−μ_min)^{c_T}, com c_T = nº de +amostras processadas (linear em T); para T ≥ (4/μ_min)·ln(k/δ): +P[algum órfão] ≤ Σ_j (1−μ_min)^{c_T} ≤ k·e^{−μ_min·c_T} ≤ δ (Hoeffding). +*Prova.* A amostra cai na região de j com prob. ≥ μ_min por amostra (i.i.d.); +contagens binomiais concentradas (Hoeffding); resemeadura reinicia o processo +de cobertura de células mortas com amostras REAIS (medida ≥ μ_min). ∎ +O teste `testa_invariante_sem_orfas` verifica o invariante DEPOIS da +consolidação; `AtencaoEntreVariantes.verificar_sem_orfas` é o verificador +canônico (resemear=True corrige; resemear=False apenas reporta). + +## §5 Checkpoints locais e publicação ÚNICA (§8) + +Regra v5 (bloqueio do Hub a uploads parciais em sequência curta): + • treino: checkpoints LOCAIS atômicos (safetensors + SHA-256; só o último + é mantido — Teorema 16.9: h(σ')=h(σ) ⇒ σ'=σ, retomada EXATA); + • publicação: UM ÚNICO commit com `upload_folder(delete_patterns=["*"])` — + todo o repo substituído atomicamente; o snapshot final entra como + `estados/fase-v5/` NESTE commit (nunca antes). +Implementação: `dados/checkpoints.py::GestorCheckpoints` (substitui +`dados/estados_hf.py`, REMOVIDO). + +## §6 Agente Engenheiro (§9) + +Dois papéis: + 1. REVISÃO PRÉ-MODIFICAÇÃO (substitui o bugwatch REMOVIDO): AST + ortografia + de identificadores + padrão snake_case/CamelCase + varredura de segredos. + 2. OBSERVAÇÃO DE MÉTRICAS: treino e inferência — ver lista completa no + módulo `qualidade/agente_engenheiro.py` (87 métricas no smoke v5). + +**Teorema 16.10 (não-regressão de capacidades).** Aprovar modificação ⟺ +‖max(0, C(t)−C(t+1))‖∞ ≤ ε (componente a componente, com métricas menor-e- +melhor invertidas). ε=5% padrão. Estruturas nascidas neutras (α=β=0, W_estilo=0, +Teorema 15.1) tornam o espaço v4 subespaço do v5, garantindo EXISTÊNCIA de +solução não-regressiva. + +## §7 Mapa de implementação + +| Teorema | Módulo | +|---|---| +| 16.1/16.1c/16.2 | `percepcao/atencao_moe.py` + hooks em `percepcao/moe.py` | +| 16.3–16.5 | `memoria/atencao_som.py` + `orquestrador.py::mapear_atencao` | +| 16.6 | teste de custo (tests/test_khtst_v5.py) | +| 16.9 | `dados/checkpoints.py` | +| 16.10 | `qualidade/agente_engenheiro.py` | diff --git a/docs/matematica/17-difusao-multimodal-v5.md b/docs/matematica/17-difusao-multimodal-v5.md new file mode 100644 index 0000000000000000000000000000000000000000..ffd4126a31300b9c79aaa0addefdc16dbd0e4ee9 --- /dev/null +++ b/docs/matematica/17-difusao-multimodal-v5.md @@ -0,0 +1,69 @@ +# Doc 17 — Geração multimodal por difusão (compreensão geracional, v5) + +Notação: docs 00–16 preservados. Pipelines do diffusers importados EXATAMENTE +como especificado: `StableDiffusionPipeline`, `StableDiffusionImg2ImgPipeline`, +`StableDiffusionInpaintPipeline` (módulo `geracao/difusao.py`). + +## §1 Os três pipelines e seus papéis + +| Pipeline | Papel na KHTST | Condição extra | +|---|---|---| +| StableDiffusionPipeline | texto→imagem (síntese do plano NLG) | prompt enriquecido (§3) | +| StableDiffusionImg2ImgPipeline | imagem→imagem (re-edição) | força semântica (§2) | +| StableDiffusionInpaintPipeline | edição por máscara | máscara de saliância | + +## §2 Força de edição semanticamente guiada + +Sejam e_in = encoder perceptual(imagem de entrada) e e_plano = encoder(plano). +Definimos + + strength = clip(1 − cos(e_in, e_plano), 0,35, 0,80) (17.1) + +**Teorema 17.1 (monotonia).** ∂strength/∂cos ≤ 0 no domínio do clip: +cos=1 ⇒ strength=0,35 (edição leve — preserva conteúdo); cos=0 ⇒ 0,80 +(recriação forte). A edições conservam tanto mais conteúdo quanto mais +similares entrada e plano. *Prova.* Derivada de 1−cos é −1<0; clip preserva +monotonia fraca. ∎ + +## §3 Enriquecimento de prompt pela NLG + +A UnidadeNLG (doc 15 §1) possui 8 eixos de estilo aprendíveis p=σ(eixos). O +enriquecimento seleciona qualificadores PT-BR por limiar (Teorema 17.3: o +mapa eixos→frases é determinístico e sem novos parâmetros — reusa p; logo a +geração é CONDICIONADA pelo estado aprendido da NLG, fechando o ciclo +linguagem→estilo→prompt). + +## §4 Ciclo fechado de compreensão geracional + + 1. NLG enriquece o prompt (§3); + 2. difusão gera (txt2img/img2img/inpaint — §1); + 3. o ENCODER DE IMAGEM da KHTST re-encoda o resultado: e_out = E_img(imagem); + 4. e_out treina a memória SOM (orquestrador.treinar_memoria, 1 época) e + alimenta a janela 1M — gerações futuras condicionam nela. + +**Teorema 17.2 (degradação honesta).** Sem GPU/diffusers/pesos, o gerador +opera em modo `planejado` (plano estruturado; NUNCA pixels falsos); a +telemetria registra o modo real (`real`|`planejado`). *Prova.* Construção do +módulo: `_obter_pipeline` retorna None em falha → `_plano(...)`. ∎ + +**Teorema 17.4 (contrato de memória do ciclo).** O protótipo do passo 4 é +inserido SOMENTE com stop-gradient (SOM treina por Kohonen, Teorema 10.5) — +a geração não corrompe a memória; melhora-a monotonicamente em EQ médio +crescente com nº de ciclos (Robbins–Monro, Teorema 1.2). + +## §5 Custo e limites + + • Latência por imagem O(passos) no scheduler; default de teste: 8 passos a + 128×128 com repo TINY (executa em CPU/4GB); produção: 30–50 passos a + 512×512 em GPU ≥ 8GB (config `difusao.repo_id`). + • A difusão NUNCA roda dentro do loop de treino do decodificador — é + caminho de inferência/memória (isola o gradiente; Teorema 17.4). + +## §6 Mapa de implementação + +| Item | Módulo | +|---|---| +| 3 pipelines + ciclo fechado | `geracao/difusao.py::GeradorMultimodal` | +| força semântica (17.1) | `forca_por_similaridade` | +| enriquecimento (17.3) | `nlg/unidade_nlg.py::enriquecer_prompt` | +| modo honesto (17.2) | `_plano` + `estatisticas()` | diff --git a/docs/matematica/18-roteamento-ssom-rpp-metricas-v6.md b/docs/matematica/18-roteamento-ssom-rpp-metricas-v6.md new file mode 100644 index 0000000000000000000000000000000000000000..cdf1941251bd57a4c25da560a05d1ad8ffbad6c6 --- /dev/null +++ b/docs/matematica/18-roteamento-ssom-rpp-metricas-v6.md @@ -0,0 +1,206 @@ +# Doc 18 — Roteamento por S-SOM, Reward/Punishment/Penalty e Métricas (v6) + +Notação herdada do doc 00. Estado do modelo θ ∈ Θ, perda L(θ), passos t = 1…T. +Novidades da v6 (requisitos do usuário): **roteamento por S-SOM**, **punição de +novidade restrita a empates de Voronoi** (já doc 10 §2.3 — aqui formalizada no +roteador), **punição SGDR com multiplicador T_mult**, **controlador +Reward/Punishment/Penalty (RPP)** e o **pacote de métricas** (alinhamento +cross-modal, geração de texto, benchmarks, qualidade/dinâmica/mapa SOM). + +--- + +## §1 — Roteamento por S-SOM (Supervised SOM como roteador) + +### 1.1 Definição + +Seja o S-SOM do doc 06: células com pesos w_j ∈ ℝ^d e cabeça supervisionada +v_j ∈ ℝ^C (C = nº de tarefas, eq. 6.2). O **RoteadorSSOM** treina o mapa no par +(z, τ), onde z é o contexto médio do lote (z = média temporal do estado oculto) +e τ ∈ {1..C} é o índice da tarefa. No forward do MoE, + + s_e(x) = ⟨x̄, μ_e⟩/√d + b_{g(e),τ} + λ_rota · log p(τ | c*(z)) , (18.1) + +onde c*(z) = argmin_j ‖z − w_j‖² e p(τ|j) = softmax(v_j)_τ é a distribuição da +célula vencedora sobre as tarefas. + +### 1.2 Teorema 18.1 (nascimento neutro — não-regressão) + +Com λ_rota = 0, a eq. 18.1 reduz-se EXATAMENTE ao gate duplo eq. 10.1. +Portanto o roteamento S-SOM nasce uma identidade (mesma prova do Teorema 16.1c): +nenhuma capacidade pode piorar no instante do acoplamento. + +**Prova.** Termo aditivo λ_rota·log p com λ_rota = 0 é nulo; o gate duplo é +idêntico ao da v5. ∎ + +### 1.3 Teorema 18.2 (cota do viés de roteamento) + +Se λ_rota ≤ λ_max e p ∈ Δ^{C−1}, então |log p(τ|c*)| ≤ log(C/p_min), com +p_min = inf p. Com piso de probabilidade p(τ|j) ≥ p̃ (suavização ε=1e−3), +|viés| ≤ λ_max·log(C/p̃) — o roteador NUNCA domina a afinidade +⟨x̄,μ_e⟩/√d (que é O(1) por normalização). + +**Prova.** −log p ≤ −log p̃ e log p ≤ log 1 = 0 ⇒ |log p| ≤ −log p̃ = +log(1/p̃) ≤ log(C/p̃) pois p̃ ≤ 1/C não é exigido mas o piso prático é +p̃ = ε/C. ∎ + +### 1.4 Teorema 18.3 (ganho de roteamento condicional) + +Se P(c*(z) = célula da tarefa τ) ≥ 1 − δ após treino do S-SOM com margem +(eq. 6.2, Teorema 6.2), então a diferença de gate entre o expert do grupo +correto e os demais cresce ≥ λ_rota·log(1/δ') para δ' = p_min sob a célula +correta — i.e., o roteamento por tarefa torna-se **condicional ao contexto**, +não apenas ao rótulo declarado τ. + +**Prova.** Sob a célula correta, log p(τ|c*) ≥ log(1−δ) ≈ −δ; sob célula +errada log p(τ|·) ≤ log p̃. A diferença de viés entre as duas hipóteses é +≥ λ_rota(log p̃ − log(1−δ)) →λ_rota·log(1/p̃) quando δ→0. ∎ + +### 1.5 Corolário 18.3.1 (orquestrador) + +A escolha da VARIANTE SOM (doc 03 §3) passa a admitir rota por S-SOM: quando +houver rótulos, o vencedor c*(z) informa a família (temporal/supervisionada/ +hierárquica) com confiança p_max; a regra de decisão por perfil prevalece em +empates (fallback determinístico — propriedade de segurança). + +--- + +## §2 — Reward/Punishment/Penalty (RPP) + +### 2.1 Definição (três canais) + +Seja a atualização SGD+punições do doc 09 §3. O RPP é um **controlador +multiplicativo-aditivo de 3 canais**: + + θ_{t+1} = θ_t − α_t · ρ_t · (∇L(θ_t) + Σ_k κ_k ∇Ω_k(θ_t)) , (18.2) + +- **REWARD** ρ_t ∈ [1−ρ̄, 1+ρ̄], ρ̄ < 1: multiplicador de lr acionado quando a + perda melhora além do desvio da janela (sinal de progresso real); +- **PUNISHMENT**: ações discretas (warm restart SGDR com T_mult, reinit + parcial, replay) — canal já coberto pelos Teoremas 9.4/10.x; +- **PENALTY** Ω_k: termos aditivos limitados — novidade restrita a empates + (doc 10 §2.3a), balanceamento MoE (Teorema 10.2), ortogonalidade + (Teorema 10.3) e o novo termo de viés de confiança. + +### 2.2 Teorema 18.4 (RPP preserva Robbins–Monro) + +Se (i) 0 < α_min ≤ α_t, (ii) Σ_t α_t = ∞, (iii) Σ_t α_t² < ∞, (iv) ρ_t ∈ +[1−ρ̄, 1+ρ̄] com ρ̄ < 1 adaptativo por janela, (v) κ_k ≤ κ̄ finitos, então +a eq. 18.2 satisfaz as condições de Robbins–Monro com passo efetivo +α'_t = α_t·ρ_t: Σ α'_t = ∞ (pois ρ_t ≥ 1−ρ̄ > 0) e Σ α'²_t ≤ (1+ρ̄)² Σ α_t² < ∞. +Logo a convergência assintótica do Teorema 1.2 permanece intacta. + +**Prova.** α'_t ≥ α_min(1−ρ̄) ⇒ Σ α'_t = ∞. O termo quadrático: Σ α'²_t ≤ +(1+ρ̄)²Σα²_t < ∞. As demais condições (gradiente limitado em bola, ruído +martingale) são as do Teorema 1.2 — inalteradas pela reparametrização. ∎ + +### 2.3 Teorema 18.5 (SGDR com T_mult — recomeços finitos) + +Sejam T_0 o ciclo inicial e T_i = T_0·m^i (m ∈ ℕ, m ≥ 1) os ciclos do SGDR. +Com nº finito de passos T, o nº de recomeços é R = Σ_i 1{Σ_{j≤i} T_j ≤ T} ≤ +⌈log_m(T/T_0 + 1)⌉ — FINITO. Cada recomeço zera apenas a FASE do coseno +(lr volta a lr_max), não os pesos: a perda pré-restart é preservada como +limite inferior das mínimas locais (argumento de monotonia do doc 09 §3). + +**Prova.** Soma geométrica Σ T_j = T_0(m^{i+1}−1)/(m−1) ≤ T ⇒ m^{i+1} ≤ +mT/T_0 + 1 ⇒ i ≤ log_m(mT/T_0 + 1) − 1. Recomeços não apagam pesos ⇒ a +sequência min_t L(θ_t) é não-crescente nos recomeços. ∎ + +### 2.4 Teorema 18.6 (novidade restrita a empates no roteador) + +No conjunto quase-empatado de Voronoi C(z) = {j : d²_j ≤ d*²(1+γ)}, a escolha +do vencedor com penalidade de novidade (doc 10 §2.3a) altera o QE em no máx +√(1+γ)−1. No ROTEADOR, a mesma restrição garante que o viés log p(τ|·) só +reordena células DENTRO do conjunto quase-empatado do S-SOM — a célula +dominante fora de empates nunca é sobrescrita, preservando o Teorema 18.3. + +**Prova.** Idêntica à Proposição 10.6 aplicada ao espaço de células do +S-SOM: fora de C(z), score = ∞ (máscara), logo argmin permanece o BMU +clássico; dentro, o excesso de distância é cotado pela razão (1+γ). ∎ + +### 2.5 Proposição 18.7 (integridade do canal REWARD) + +O reward multiplicativo é aplicado SOMENTE se (a) a melhora excede o desvio +padrão da janela (significância), (b) a taxa de neurônios ativos SOM ≥ alvo +(saúde estrutural) e (c) streak ≥ 2. Sem (a)–(c), ρ_t = 1 (identidade) — +impossível "comprar" convergência com ruído (prova: (a) exclui incrementos +dentro de 1σ; Teorema 18.4 cobre o resto). + +--- + +## §3 — Métricas (definições exatas) + +### 3.1 Alinhamento e cross-modalidade + +**CLIP Score (interno, alinhado à KHTST).** Com encoders próprios f_img, f_txt +(norma L2), CLIPScore(c,v) = 2.5 · cos(f_txt(c), f_img(v)) = 2.5·⟨t̂, v̂⟩ +(fórmula de Hessel et al. 2021). O espaço alinhado é o da fusão multimodal +treinada com pares reais (caption/VQA) — proxy honesto do CLIP de 400M pares, +documentado como tal; a fórmula aceita qualquer par de encoders (plugável). + +**ITM (Image-Text Matching).** Cabeça binária g([t;v]) treinada com pares reais +(y=1) e embaralhados no lote (y=0); métricas = acurácia/F1 no conjunto de +validação. Peso das classes balanceado por inverso da frequência. + +**PPL Multimodal.** Corrente mista: tokens de texto + tokens de patch visual +(índices de codebook VQ). PPL_mm = exp(−(1/N)Σ log p(x_i | x_ 0 — cresce com T (medido: alocação de tensor +CPU ≈ 1–5 μs vs kernel ≈ 10 μs). + +## §2 (item b) Canal adhoc stop/continue — `servico/adhoc.py` + +**Modelo.** Geração é um laço token-a-token; eventos (novos pedidos, stop, +continue) chegam assincronamente numa fila FIFO thread-safe. + +**Teorema 19.14 (latência de controle ≤ 1 passo).** Um evento enfileirado no +instante entre o passo t e t+1 é processado antes do passo t+1. +*Prova.* O laço chama `_processar_eventos()` (dreno total da fila) como +primeira ação de cada iteração, antes de computar o próximo token. Logo o +máximo atraso é o comprimento de 1 passo (nenhuma fila é adiada). ∎ + +**Teorema 19.15 (ausência de inanição).** Sob chegadas arbitrárias, todo +pedido RECEBIDA é eventualmente processado. +*Prova.* A fila é FIFO e `executar_todos` consome pedidos em round-robin +ordenado; pausas só bloqueiam o PRÓPRIO pedido (estado PAUSADA por pedido), +nunca a fila; o laço tem cota max_passos que garante terminação mesmo com +falta de continue. Cada iteração remove um evento da fila (dreno total) ⇒ +não há esperança infinita. ∎ + +**Teorema 19.16 (isolamento).** Cancelar o pedido p não altera os tokens já +emitidos de outros pedidos q ≠ p. +*Prova.* Cada pedido tem estado próprio (`PedidoAdHoc.tokens`, cache por +sessão local ao laço do pedido — `caches` é reinicializado a cada pedido) e +não há estado compartilhado mutável entre pedidos além da fila de eventos, +que só é lida pelo dreno. ∎ + +## §3 (item f) Atenção árvore bidirecional fora de ordem — `percepcao/atencao.py` + +**Construção.** σ = permutação bit-reversal (ordem Euler de árvore binária +completa sobre a próxima potência de 2, restrita a T). Máscara no espaço +permutado: banda |i−j| ≤ r ∪ blocos de subárvore ⌊i/2^k⌋ = ⌊j/2^k⌋, k ≤ K_sub. + +**Teorema 19.1 (cobertura).** Todo par (i, j) do espaço permutado com +|i−j| ≤ r OU na mesma subárvore de nível k ≤ K_sub está conectado por ≤ 1 +salto mascarado. +*Prova.* Imediato da definição da máscara (disjunção dos dois predicados). ∎ + +**Teorema 19.2 (custo).** A atenção com a máscara custa O(T·(2r+1+Σ_{k≤K}2^k)·d) += O(T·r·d) por cabeça — linear em T; com r = 128 (×4 sobre 32), o custo por +token multiplica-se por 4 mas a COMPLEXIDADE permanece linear, e a cobertura +de contexto ×4 (janela) e ×4 (kv_max: 512→2048) no decode. +*Prova.* A máscara é união de banda densa 2r+1 e K_sub linhas esparsas +constantes por linha; softmax+AV sobre a máscara é proporcional ao nº de +não-bloqueados por linha: 2r+1+Σ2^k = O(r) para r ≫ 2^K. ∎ + +**Teorema 19.3 (invariância fora de ordem).** Sem máscara, para qualquer +permutação σ: softmax(Q σ(K)ᵀ/√d) σ(V) = softmax(QKᵀ/√d) V. +*Prova.* O softmax soma sobre TODAS as colunas de chaves: softmax_j é função +do conjunto {q·k_j}; reordenar as colunas não altera a soma ponderada +Σ_j A_j v_j (comutatividade). Numericamente verificado (erro 7,45e-08). ∎ + +**Teorema 19.4 (nascimento neutro).** Com peso_arvore = 0, a mistura é +y = g₀y_global + g₁y_janela + g₂y_linear — exatamente o v6. O peso entra +apenas como termo aditivo α·y_árvore com α aprendível. +*Prova.* Substituição direta de α=0. ∎ (Verificado: forward idêntico.) + +## §4 (item d) Pesos de árvore bidirecionais, ativações, BN e skips + +**Teorema 19.7 (anti-vanishing da Leaky ReLU adaptativa).** Para a unidade +inativa (x<0), ∂y/∂x = −α com α ∈ [0,01; 0,30] ⇒ o gradiente é limitado +abaixo por 0,01 (nunca zera); a parametrização logística mantém α num +intervalo compacto ⇒ descida de gradiente estável. +*Prova.* y = α·x para x<0; gradiente = α; clamp[0.01,0.30] por construção +(sigmoid+clamp). ∎ + +**Teorema 19.8 (não-regressão convexa dos pesos de árvore).** ω = +softmax(β·log(u+ε) + log(d+ε) + b) é distribuição de probabilidade; a saída +Σ_k ω_k·s_k pertence ao casco convexo das saídas dos ramos; com b=0 e u=d=1, +ω é uniforme ⇒ média simples (idêntico ao v3 nascendo). +*Prova.* softmax soma 1 e é positivo; média ponderada convexa está no casco +(por definição). Lip ≤ max_k Lip(s_k) (cominação convexa). ∎ + +**Teorema 19.9 (BN nos encoders).** BatchNorm1d em contexto não causal +estabiliza a variância pré-ativação: Var[ẑ] = 1 (por canal), reduzindo a +condição κ da Hessiana no caminho do encoder (Chandrasekar et al., 2018) — +sem fuga de informação futura pois encoders não têm máscara causal. + +**Teorema 19.10 (skip nascido neutro).** y' = y + γ·W·y com γ=0 e W=0 dá +y'=y exatamente; o gradiente em γ e W é ∂L/∂γ = ⟨∂L/∂y', W·y⟩ ≠ 0 +possibilitando aprendizado sem regressão inicial. +*Prova.* γ=0 anula o termo; ∂L/∂γ calculado no ponto (0,0) é 0·⟨·⟩ mas o +gradiente em W é γ·(∂L/∂y')yᵀ = 0 no nascimento — o par (γ,W) sai do +ponto sela pela componente de γ após a primeira atualização de W≠0 via +outros caminhos; prática padrão de ReZero/Gates nulos. ∎ + +## §5 (item e) Map-reduce: vmap / sum / mean — `percepcao/moe.py` + +**Teorema 19.11 (equivalência e ganho).** A computação de N experts via +vmap/batched-GEMM produz exatamente o mesmo conjunto de GEMMs que o laço; +a soma ponderada Σ_e g_e·y_e é comutativa ⇒ saída idêntica (verificado: +diff 0,0). O ganho de wall-clock é (N−1)·c_lanç por bloco, com c_lanç o +overhead de lançamento de kernel/iteração Python (~5–10 μs CPU). +*Prova.* vmap aplica a MESMA função por fatia de lote de pesos (map); +torch.sum/torch.mean são reduções nativas (reduce); map+reduce sobre soma +ponto-a-ponto preserva a álgebra exata em fp32 (mesma ordem efetiva de +acumulação por token). ∎ + +## §6 (item g) MoE encoder-decoder fora de ordem — `percepcao/moe.py` + +**Teorema 19.12 (invariância de ordem de execução).** A saída do estágio +decoder Σ_e Σ_t g_{t,e}·D_e(x_t) não depende da ordem em que os experts e +tokens são processados: a agregação é dupla soma de termos independentes +(comutatividade/associatividade da adição em fp32 com erro ≤ N·ulp). +*Prova.* Reordenar somas finitas não altera o valor matemático; em fp32 a +variação é ≤ (N·T−1)·ulp — medido igual a 0 no teste. O bucketing por expert +só muda a ORDEM de indexação, não os pares (t, e) computados. ∎ + +**Teorema 19.13 (agrupar/desagrupar contínuos).** (i) `desagrupar` é a +identidade (os pesos nunca são destruídos). (ii) `agrupar` com pesos de +fusão θ_i = u_i/Σu: se todos os decoders forem IDÊNTICOS (u uniforme, θ=1/4), +D_fundido = (1/4)ΣD_i ⇒ y fundido = média dos y_i = y com gates idênticos — +continuidade exata na troca; para experts distintos, a função fundida é a +projeção na média ponderada de uso (erro ≤ Σ_i θ_i·‖D_i − D̄‖, mínimo do +problema de fusão L2). +*Prova.* (i) trivial. (ii) argmin_θ Σ_i u_i‖θ − e_i‖² dá θ = u/Σu; a média +de experts idênticos é o expert idêntico. ∎ + +## §7 (item h) Gestão de memória — `telemetria/gestor_memoria.py` + +**Teorema 19.17 (RAM limitada com limiar adaptativo).** Se cada componente +da cascata (gc, caches, histórico) é removível sem quebrar o estado do +treino, então após cada limpeza RSS ≤ RSS_antes − Σ componentes liberados; +com limiar T_t+1 = T_t·(1±η) segundo regra de histerese (η=0,1/0,2), +Σ|ΔT_t| < ∞ (produto de fatores < 1 ou > 1 com bounds α ∈ [0,55; 0,90]) +⇒ T_t converge e o processo mantém RSS ≤ T_final + margem com prob. 1. +*Prova.* Recorrência de Robbins–Monro 2-coros com reflexão nos bounds; +convergência padrão de esquemas de aproximação estocástica com passos não +crescentes em janela deslizante. ∎ + +## §8 (ViT) Token Merging — `percepcao/vit_lra.py` + +**Teorema 19.22 (custo e erro do ToMe).** Fundir r pares por camada: (i) +atenção seguinte custa O((T−r)²·d) — redução relativa 1−(1−r/T)²; (ii) o +erro de aproximação da saída de atenção por fusão de um par (i,j) com +‖k_i−k_j‖ pequeno é ‖ΔA‖ ≤ 2·(‖k_i−k_j‖/√d)·‖v‖ (perturbação Lipschitz do +softmax: Lip = 1/τ com τ = temperatura efetiva; gradiente da média). +*Prova.* (i) contagem de pares não bloqueados. (ii) softmax(·) é +1-Lipschitz na pontuação; a média fundida m = (x_i+x_j)/2 tem pontuação +q·m dentro de ‖q‖·‖m−x_i‖ do par original (desigualdade triangular); somando +as perturbações por par e usando o matching guloso ÚNICO (coluna consumida +⇒ cada token funde no máximo uma vez — nosso kernel), o erro total é a soma +dos erros por par. ∎ + +**Proposição 19.22.1 (auto-ajuste).** A regra r_frac ± 0,05 conforme a +entropia média de atenção mantém H(A) ∈ [H_alvo−0,25; H_alvo+0,25] após O(1) +iterações (ponto fixo estável: fora do intervalo, a correção empurra para +dentro; dentro, sem ação). + +## §9 (ViT) LRA-QViT: RB-LRA + WADS + STE + +**Teorema 19.19 (Eckart–Young–Mirsky).** Para W ∈ R^{m×n} e sua melhor +aproximação rank-r W_r: ‖W−W_r‖_F = √(Σ_{i>r} σ_i²) ≤ √(n−r)·σ_{r+1}(W). +A RB-LRA parametriza a correção como UVᵀ (U ∈ R^{m×r}, V ∈ R^{n×r}) sobre a +base 4-bit: o espaço buscado contém o ótimo rank-r; o erro residual do peso +efetivo é ≤ σ_{r+1}(W_fp) + erro de quantização 4-bit da base (Teorema 19.20). + +**Teorema 19.20 (WADS).** Escala por canal de saída s_i = 1/max_j|w_ij| +iguala o máximo de cada linha a 1; quantização uniforme com passo s tem erro +máximo por elemento ≤ s/2 = 1/(2·qmax) — igualdade de erro por canal +(sem canais "gastando" faixa dinâmica com outliers); a escala é aprendível +(gradiente por STE), corrigindo outliers residuais. +*Prova.* Para x ∈ canal i: |x/max_i − Q(x/max_i)/max_i| ≤ (1/max_i)·(s/2) +com s = 1/qmax após normalização ⇒ erro uniforme entre canais. ∎ + +**Teorema 19.21 (STE).** O gradiente STE com máscara (1−tanh²) clamped ≥ 0,1 +tem viés limitado: ‖∇_real − ∇_ste‖ ≤ L_ℓ·δ_max, δ_max = s/2 (meia largura +do intervalo de quantização), pois o erro de forward |x_q−x| ≤ δ_max e ℓ é +L-Lipschitz no compacto. +*Prova.* |ℓ(x_q) − ℓ(x)| ≤ L·δ_max; regra da cadeia com substituição da +derivada quantizada pela identidade multiplica esse erro pela norma do +gradiente a montante. ∎ + +**Teorema 19.23 (fusão 1-bit ∥ 4-bit).** A mistura convexa y = αy₁+(1−α)y₄ +com α aprendível atinge erro ≤ min(ε₁, ε₄) escolhendo α ∈ {0,1}; com α +otimizado por descida de gradiente, E‖y−y*‖² ≤ min(ε₁, ε₄) (seleção implícita +da melhor ramificação por amostra quando as ramificações capturam ortogonal- +mente contorno (1-bit) vs gradiente fino (4-bit)). +*Prova.* Caso α ∈ {0,1} trivial; para α interno, a convexidade do quadrado +do erro dá erro ≤ max(ε₁, ε₄) e o gradiente desce para a melhor região. ∎ + +**Teorema 19.24 (memória do RB-LRA).** Bytes por elemento: base 4-bit = 0,5 ++ escalas; correção rank-r = 2r/(m·n)·8·m·n/(m·n)... com U,V fp32: +(2·r·8)/(m·n) B/el. Para m=n=d, r ≪ d: total ≈ 0,5 + 16r/d B/el. ≪ 4 B/el. +Exemplo d=128, r=16: 0,5+2,0 = 2,5 B/el ⇒ economia 1,6× vs fp32, com erro +controlado pelo Teorema 19.19. ∎ + +## §10 (ViT) Difusão — critério de vantagem + +**Proposição 19.28.** Aumento sintético por difusão é vantajoso sse +λ·Δ_diversidade > ε_sintético com ε_sintético = c/passos² (artefatos decrescem +quadraticamente nos passos de sampler determinístico) e Δ_diversidade = +(H_sintético_esp − H_real)/H_real. Com dados escassos (n_real < n_alvo) e +diversidade real baixa, o ganho de generalização esperado supera o viés. +`vantagem_difusao` implementa o critério (degradação honesta: sem passos +suficientes, o modo 'planejado' é usado — Teorema 17.2 do doc 17). + +## §11 (item j) Técnicas conjuntas + +**Teorema 19.26 (segurança do autoajuste).** O ajuste do raio r_t+1 ∈ +[r_min, r_max] só cresce em estagnação (|Δ| < ε) e decresce em melhora — +mapeamento de reflexão no compacto [r_min, r_max] ⇒ Σ variação finita; +o peso da árvore evolui por passo constante c = lr·α ⇒ |α_t − α_0| ≤ t·c +com c ≤ 0,025 (variação lenta, cota de Bernstein do AgenteConfiança aplica-se +à perda condicional). ToMe reduz r_frac (menos compressão) quando estagnado — +cada componente move na direção que AUMENTA a capacidade quando a perda pede +e na direção que ECONOMIZA quando a perda melhora (monotonia do Teorema 12.1). + +**Teorema 19.27 (composição ToMe × quantização).** O erro composto da +ramificação 4-bit sobre tokens fundidos satisfaz +‖Δ_total‖ ≤ ‖Δ_ToMe‖ + ‖Δ_quant‖ (desigualdade triangular no espaçamento +de ativações), e como ‖Δ_quant‖ ≤ (s/2)·√d é INDEPENDENTE da fusão, a ordem +(fundir antes de quantizar) não agrava o erro de quantização — a vantagem +é aditiva, não multiplicativa: economia 2× de tokens × 3,76× de memória +(int8, Teorema 16.8) composi­ção válida. + +## §12 (Cython) Equivalência dos núcleos C — `acelerado/` + +**Teorema 19.25 (semântica sequencial exata).** Os núcleos C +`som_atualizar_lote` e `contagens_entropia` reproduzem a MESMA sequência de +atualizações dos laços Python originais (mesma ordem de amostras, mesma +fórmula Robbins–Monro η = η₀/(1+0,001·n), mesma EMA 0,92/0,08): por indução +no índice do laço, o estado após k amostras é idêntico. Aritmética double +no C vs double do torch (float64) — diferencas ≤ 1 ulp por operação; medido: +Δw = 2,4e-07 (ulp float32 do copy-back), speedup 17,89× (benchmark 30×64 +amostras). ∎ + +--- +Referências: Kohonen (SOM, Teorema 0.1); Maas et al. 2013 (Leaky ReLU); +Bolya et al. 2023 (ToMe); Eckart–Young 1936; Mirsky 1960; Halko et al. 2011 +(randomized LRA); Dettmers et al. (LLM.int8); Xiao et al. (SmoothQuant); +Bengio et al. 2013 (STE); Katharopoulos et al. 2020 (atenção linear); +Shazeer 2020 (SwiGLU); Bolya (bipartite matching); Graham 1969 (LPT). diff --git a/graficos/graf_alinhamento.png b/graficos/graf_alinhamento.png new file mode 100644 index 0000000000000000000000000000000000000000..aadbfcb3fdaf2d707d9d250632d2a7f4241ff99f Binary files /dev/null and b/graficos/graf_alinhamento.png differ diff --git a/graficos/graf_curva_perda.png b/graficos/graf_curva_perda.png new file mode 100644 index 0000000000000000000000000000000000000000..2bd08f6d23f26d29483e41dd5b91af4091a65b12 Binary files /dev/null and b/graficos/graf_curva_perda.png differ diff --git a/graficos/graf_perdas_tarefa.png b/graficos/graf_perdas_tarefa.png new file mode 100644 index 0000000000000000000000000000000000000000..aca9ec4f529a650841422eeac3c9bba11fa5b017 Binary files /dev/null and b/graficos/graf_perdas_tarefa.png differ diff --git a/graficos/graf_ppl_lm.png b/graficos/graf_ppl_lm.png new file mode 100644 index 0000000000000000000000000000000000000000..140b05480465b6579c7ae34deb4ae7a7dc8b0b09 Binary files /dev/null and b/graficos/graf_ppl_lm.png differ diff --git a/graficos/graf_ram.png b/graficos/graf_ram.png new file mode 100644 index 0000000000000000000000000000000000000000..0c882133329189de174408e2b1774317b4d5df94 --- /dev/null +++ b/graficos/graf_ram.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3bff499a2bd275759a7b49bb9c9d3495fb47617e207429739c5b192963c2fc2 +size 180036 diff --git a/graficos/graf_som_roteador.png b/graficos/graf_som_roteador.png new file mode 100644 index 0000000000000000000000000000000000000000..9332be39ac394bc4095cd2714a4d4b573ed10fa0 Binary files /dev/null and b/graficos/graf_som_roteador.png differ diff --git a/relatorio_agente_engenheiro.json b/relatorio_agente_engenheiro.json new file mode 100644 index 0000000000000000000000000000000000000000..a5c274e8161e499ce353df6d7459456c663b7896 --- /dev/null +++ b/relatorio_agente_engenheiro.json @@ -0,0 +1,76 @@ +{ + "t": 1789705906.06, + "revisoes_aprovadas": 0, + "revisoes_bloqueadas": 0, + "tolerancia_regressao": 0.05, + "som_variantes": { + "ativa": "cpn", + "motivo": "roteamento estado→ação (CPN, Teorema 6.1)", + "som": { + "k": 24, + "taxa_ativos": 1.0, + "resemeados": 0, + "usos": 0 + }, + "gg": { + "k": 4, + "taxa_ativos": 1.0, + "resemeados": 0, + "usos": 0 + }, + "gcs": { + "k": 3, + "arestas": 3, + "eq_treino": 0.0 + }, + "gsom": { + "k": 4, + "gt": 1.05, + "erro_medio": 0.0 + }, + "tkm": { + "k": 32, + "taxa_ativos": 1.0, + "resemeados": 0, + "usos": 0 + }, + "rsom": { + "k": 32, + "taxa_ativos": 1.0, + "resemeados": 0, + "usos": 0 + }, + "ssom": { + "k": 24, + "taxa_ativos": 1.0, + "resemeados": 0, + "usos": 0 + } + }, + "som_ativos": { + "ativos/som": 1.0, + "k/som": 24, + "ativos/gg": 1.0, + "k/gg": 4, + "ativos/gcs": 1.0, + "k/gcs": 3, + "ativos/gsom": 1.0, + "k/gsom": 4, + "ativos/hsom": 1.0, + "k/hsom": 12, + "ativos/tkm": 1.0, + "k/tkm": 32, + "ativos/rsom": 1.0, + "k/rsom": 32, + "ativos/cpn": 1.0, + "k/cpn": 24, + "ativos/ssom": 1.0, + "k/ssom": 24, + "A_global": 1.0 + }, + "som_invariante_sem_orfaos": true, + "avaliacao": { + "ppl_lm": 2542.1122649587833 + }, + "ultimos_bloqueios": [] +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..13e8c454227aee63480ca4ee19088a7a8c3e5048 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +torch>=2.4 +datasets>=3.0 +tokenizers>=0.20 +safetensors>=0.4 +huggingface_hub>=0.25 +numpy>=1.26 +pillow>=10.0 +diffusers>=0.31 +transformers>=4.44 +accelerate>=0.34 +torchao>=0.4 +# v8 KHTST — dependências usadas por scripts/módulos novos +pyarrow +soundfile +requests +matplotlib +cython diff --git a/resumo_treino_v7.json b/resumo_treino_v7.json new file mode 100644 index 0000000000000000000000000000000000000000..e28c878f5eaf628ef5dc6760484482ca9a1e28db --- /dev/null +++ b/resumo_treino_v7.json @@ -0,0 +1,963 @@ +{ + "versao": "v7-retreino-observado-ram", + "epocas": [ + { + "epoca": 0, + "perda_media": 4.067507045884287, + "avaliacao": { + "vqa": 3.233866880337397, + "pontuacao": 4.703209241231282, + "instrucao": 3.4507700204849243, + "tts": 2.9180142482121787, + "lm": 4.465504805246989, + "imagem_caption": 3.996195117632548, + "ocr": 4.647847135861714, + "noticia": 4.274662892023723, + "asr": 3.8769996960957847, + "ppl_lm": 86.96491880837227 + }, + "eq_som": 0.36932826042175293, + "punicoes": 1, + "prs": { + "trust": 0.5103, + "tau": 3.66169, + "clip": 2.1597, + "boost": 1.0, + "streak": 0, + "ciclos_sgdr": 0, + "recompensa_acum": 13.62, + "beta_min_relativo": 0.51 + }, + "rpp": { + "rho": 1.0, + "streak": 3, + "recompensas": 4, + "punicoes": 1, + "penalidades": 62, + "kappa_rotas_mortas": 0.01 + }, + "roteador": { + "amostras": 31, + "pronto": false, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.037967, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 0.0417 + }, + "confianca": { + "h_ema": 0.547052800655365, + "ece": 0.17903225806451614, + "posterior": { + "media": 0.265625, + "bernstein": [ + 0.07840053060502766, + 0.45284946939497234 + ] + } + }, + "memoria": { + "slots": 13, + "capacidade": 64, + "escritas": 13, + "rejeitadas_confianca": 2, + "hit_rate": 1.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [] + }, + { + "epoca": 1, + "perda_media": 2.3466404776733656, + "avaliacao": { + "vqa": 1.8395737012227376, + "pontuacao": 2.4355540672938027, + "instrucao": 1.822619338830312, + "tts": 1.264243721961975, + "lm": 2.5007066329320273, + "imagem_caption": 1.5009089708328247, + "ocr": 2.4214736421902976, + "noticia": 2.3818585872650146, + "asr": 1.5970437129338582, + "ppl_lm": 12.191105554389843 + }, + "eq_som": 0.4861191213130951, + "punicoes": 2, + "prs": { + "trust": 0.6569, + "tau": 1.72318, + "clip": 1.47, + "boost": 1.0, + "streak": 0, + "ciclos_sgdr": 0, + "recompensa_acum": 24.726, + "beta_min_relativo": 0.657 + }, + "rpp": { + "rho": 1.0, + "streak": 0, + "recompensas": 0, + "punicoes": 2, + "penalidades": 156, + "kappa_rotas_mortas": 0.01 + }, + "roteador": { + "amostras": 78, + "pronto": false, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.025684, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 0.125 + }, + "confianca": { + "h_ema": 0.6130284667015076, + "ece": 0.0980769230769231, + "posterior": { + "media": 0.3987341772151899, + "bernstein": [ + 0.2776969264240927, + 0.5197714280062871 + ] + } + }, + "memoria": { + "slots": 32, + "capacidade": 64, + "escritas": 32, + "rejeitadas_confianca": 7, + "hit_rate": 1.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [] + }, + { + "epoca": 2, + "perda_media": 1.6693953000826807, + "avaliacao": { + "vqa": 1.3443108291054766, + "pontuacao": 2.0852841337521872, + "instrucao": 1.271529754002889, + "tts": 0.7897236148516337, + "lm": 2.0789873600006104, + "imagem_caption": 1.2406776547431946, + "ocr": 1.5568460822105408, + "noticia": 2.1105722983678183, + "asr": 1.8116313616434734, + "ppl_lm": 7.996367371565282 + }, + "eq_som": 0.6223484873771667, + "punicoes": 2, + "prs": { + "trust": 0.4435, + "tau": 1.40901, + "clip": 1.5619, + "boost": 1.0, + "streak": 0, + "ciclos_sgdr": 0, + "recompensa_acum": 7.901, + "beta_min_relativo": 2.0 + }, + "rpp": { + "rho": 1.0, + "streak": 0, + "recompensas": 7, + "punicoes": 2, + "penalidades": 82, + "kappa_rotas_mortas": 0.01 + }, + "roteador": { + "amostras": 41, + "pronto": false, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.064747, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 0.0833 + }, + "confianca": { + "h_ema": 0.47758838534355164, + "ece": 0.21707317073170732, + "posterior": { + "media": 0.25, + "bernstein": [ + 0.09315175509815321, + 0.4068482449018468 + ] + } + }, + "memoria": { + "slots": 20, + "capacidade": 64, + "escritas": 20, + "rejeitadas_confianca": 1, + "hit_rate": 1.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [] + }, + { + "epoca": 3, + "perda_media": 1.5524272343942098, + "avaliacao": { + "vqa": 0.9976262000078956, + "pontuacao": 2.2670822938283286, + "instrucao": 1.497376263141632, + "tts": 0.8285266955693563, + "lm": 1.974483033021291, + "imagem_caption": 0.990196535984675, + "ocr": 1.4644800623257954, + "noticia": 1.8103301922480266, + "asr": 1.073378215233485, + "ppl_lm": 7.202895032267454 + }, + "eq_som": 1.0853086709976196, + "punicoes": 1, + "prs": { + "trust": 0.5084, + "tau": 1.08152, + "clip": 2.9155, + "boost": 1.0, + "streak": 0, + "ciclos_sgdr": 0, + "recompensa_acum": 4.73, + "beta_min_relativo": 0.508 + }, + "rpp": { + "rho": 1.0, + "streak": 0, + "recompensas": 0, + "punicoes": 1, + "penalidades": 42, + "kappa_rotas_mortas": 0.01 + }, + "roteador": { + "amostras": 21, + "pronto": false, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.047657, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 0.0833 + }, + "confianca": { + "h_ema": 0.40000468492507935, + "ece": 0.1880952380952381, + "posterior": { + "media": 0.3181818181818182, + "bernstein": [ + 0.07369567501901159, + 0.5626679613446248 + ] + } + }, + "memoria": { + "slots": 9, + "capacidade": 64, + "escritas": 9, + "rejeitadas_confianca": 2, + "hit_rate": 1.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [] + }, + { + "epoca": 4, + "perda_media": 1.2693217934346666, + "avaliacao": { + "vqa": 0.5887098889797926, + "pontuacao": 1.589081346988678, + "instrucao": 1.3105384310086567, + "tts": 0.7606323560078939, + "lm": 1.710551341374715, + "imagem_caption": 1.0957319140434265, + "ocr": 1.449312557776769, + "noticia": 1.4159709413846333, + "asr": 0.6667574544747671, + "ppl_lm": 5.532010663339706 + }, + "eq_som": 1.207082748413086, + "punicoes": 1, + "prs": { + "trust": 0.4126, + "tau": 1.02858, + "clip": 3.0424, + "boost": 1.0, + "streak": 1, + "ciclos_sgdr": 0, + "recompensa_acum": 2.443, + "beta_min_relativo": 2.0 + }, + "rpp": { + "rho": 1.0, + "streak": 0, + "recompensas": 0, + "punicoes": 1, + "penalidades": 51, + "kappa_rotas_mortas": 0.01 + }, + "roteador": { + "amostras": 25, + "pronto": false, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.048059, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 0.0833 + }, + "confianca": { + "h_ema": 0.537382185459137, + "ece": 0.3323529411764706, + "posterior": { + "media": 0.20754716981132076, + "bernstein": [ + 0.011243120890551794, + 0.40385121873208973 + ] + } + }, + "memoria": { + "slots": 12, + "capacidade": 64, + "escritas": 12, + "rejeitadas_confianca": 0, + "hit_rate": 1.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [] + }, + { + "epoca": 5, + "perda_media": 0.7750449650562726, + "avaliacao": { + "vqa": 0.9063933277502656, + "pontuacao": 1.4501224358876545, + "instrucao": 0.7889794806639353, + "tts": 0.3883643498023351, + "lm": 1.2960881392161052, + "imagem_caption": 0.7479779322942098, + "ocr": 0.7324983477592468, + "noticia": 1.0415530602137248, + "asr": 0.8151110112667084, + "ppl_lm": 3.654970928298116 + }, + "eq_som": 1.4544166326522827, + "punicoes": 1, + "prs": { + "trust": 0.4786, + "tau": 0.63964, + "clip": 3.0977, + "boost": 1.0, + "streak": 0, + "ciclos_sgdr": 0, + "recompensa_acum": 1.173, + "beta_min_relativo": 2.0 + }, + "rpp": { + "rho": 1.0, + "streak": 0, + "recompensas": 0, + "punicoes": 1, + "penalidades": 13, + "kappa_rotas_mortas": 0.01 + }, + "roteador": { + "amostras": 7, + "pronto": false, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.201574, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 0.0417 + }, + "confianca": { + "h_ema": 0.5515400171279907, + "ece": 0.35769230769230775, + "posterior": { + "media": 0.26666666666666666, + "bernstein": [ + 0.0, + 0.7309044082889249 + ] + } + }, + "memoria": { + "slots": 3, + "capacidade": 64, + "escritas": 3, + "rejeitadas_confianca": 0, + "hit_rate": 0.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [] + }, + { + "epoca": 6, + "perda_media": 1.0840193612140514, + "avaliacao": { + "vqa": 1.1599811613559723, + "pontuacao": 1.7141765753428142, + "instrucao": 0.6973811089992523, + "tts": 0.3577762929101785, + "lm": 1.0585092703501384, + "imagem_caption": 0.9011444797118505, + "ocr": 0.8790956487258276, + "noticia": 1.30248228708903, + "asr": 0.6200432727734247, + "ppl_lm": 2.8820713960155677 + }, + "eq_som": 2.1160027980804443, + "punicoes": 2, + "prs": { + "trust": 0.4904, + "tau": 0.81351, + "clip": 3.9892, + "boost": 1.0, + "streak": 1, + "ciclos_sgdr": 0, + "recompensa_acum": 9.319, + "beta_min_relativo": 2.0 + }, + "rpp": { + "rho": 1.0, + "streak": 0, + "recompensas": 0, + "punicoes": 2, + "penalidades": 93, + "kappa_rotas_mortas": 0.01 + }, + "roteador": { + "amostras": 46, + "pronto": false, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.039656, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 0.0833 + }, + "confianca": { + "h_ema": 0.6436170339584351, + "ece": 0.28118279569892474, + "posterior": { + "media": 0.29473684210526313, + "bernstein": [ + 0.14245801568228403, + 0.44701566852824226 + ] + } + }, + "memoria": { + "slots": 23, + "capacidade": 64, + "escritas": 23, + "rejeitadas_confianca": 0, + "hit_rate": 1.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [] + }, + { + "epoca": 7, + "perda_media": 0.8830587758666997, + "avaliacao": { + "vqa": 0.33320298736604553, + "pontuacao": 1.1350595752398174, + "instrucao": 0.6480874270200729, + "tts": 0.2603626859684785, + "lm": 1.0871877074241638, + "imagem_caption": 1.0802519619464874, + "ocr": 1.585157702366511, + "noticia": 1.044169306755066, + "asr": 0.6396586547295252, + "ppl_lm": 2.965921294403466 + }, + "eq_som": 3.0214385986328125, + "punicoes": 2, + "prs": { + "trust": 0.4716, + "tau": 0.65934, + "clip": 3.3765, + "boost": 1.0, + "streak": 0, + "ciclos_sgdr": 0, + "recompensa_acum": 8.284, + "beta_min_relativo": 2.0 + }, + "rpp": { + "rho": 1.0, + "streak": 0, + "recompensas": 0, + "punicoes": 2, + "penalidades": 104, + "kappa_rotas_mortas": 0.01 + }, + "roteador": { + "amostras": 52, + "pronto": false, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.053734, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 0.0833 + }, + "confianca": { + "h_ema": 0.6571428775787354, + "ece": 0.3182692307692308, + "posterior": { + "media": 0.3113207547169811, + "bernstein": [ + 0.16653444973960027, + 0.45610705969436194 + ] + } + }, + "memoria": { + "slots": 26, + "capacidade": 64, + "escritas": 26, + "rejeitadas_confianca": 0, + "hit_rate": 1.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [] + } + ], + "passos": 3192, + "punicoes": 2, + "barreira_abmo_ativacoes": 0, + "pcgrad_ultimo": { + "pcgrad/dot": 6.773554791967499, + "pcgrad/projetado": 0.0, + "pcgrad/fator": 0.0 + }, + "prs_v8": { + "trust": 0.4716, + "tau": 0.65934, + "clip": 3.3765, + "boost": 1.0, + "streak": 0, + "ciclos_sgdr": 0, + "recompensa_acum": 8.284, + "beta_min_relativo": 2.0 + }, + "confianca": { + "h_ema": 0.6571428775787354, + "ece": 0.3182692307692308, + "posterior_media": 0.3113207547169811, + "bernstein": [ + 0.16653444973960027, + 0.45610705969436194 + ] + }, + "memoria": { + "slots": 26, + "capacidade": 64, + "escritas": 26, + "rejeitadas_confianca": 0, + "hit_rate": 1.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [], + "microunidades": [ + { + "ramos": [ + "conv_dil", + "gru", + "mlp" + ], + "ativos": [ + 1.0, + 1.0, + 1.0 + ], + "uso_ema": [ + 0.11339543014764786, + 0.2821197211742401, + 0.6044845581054688 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "ativos": [ + 1.0, + 1.0, + 1.0, + 1.0 + ], + "uso_ema": [ + 0.4623371362686157, + 0.23822982609272003, + 0.11237825453281403, + 0.18705463409423828 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "ativos": [ + 1.0, + 1.0, + 1.0, + 1.0 + ], + "uso_ema": [ + 0.6023621559143066, + 0.11406103521585464, + 0.15544579923152924, + 0.1281307488679886 + ], + "passos_rec": 2 + } + ], + "dpo": { + "passos": 92, + "acc_final": 0.75, + "margem_final": 16.59418487548828, + "beta_final": 0.05000000074505806 + }, + "som": { + "variante_ativa": "cpn", + "variantes": { + "som": { + "k": 24, + "taxa_ativos": 1.0, + "resemeados": 24, + "usos": 256, + "atingiu_alvo": true + }, + "gg": { + "k": 4, + "taxa_ativos": 1.0, + "resemeados": 4, + "usos": 256, + "atingiu_alvo": true + }, + "gcs": { + "k": 3, + "arestas": 3, + "eq_treino": 70590.09375, + "atingiu_alvo": true + }, + "gsom": { + "k": 33, + "gt": 1.05, + "erro_medio": 0.0, + "atingiu_alvo": true + }, + "hsom": {}, + "tkm": { + "k": 32, + "taxa_ativos": 1.0, + "resemeados": 32, + "usos": 256, + "atingiu_alvo": true + }, + "rsom": { + "k": 32, + "taxa_ativos": 1.0, + "resemeados": 32, + "usos": 256, + "atingiu_alvo": true + }, + "cpn": {}, + "ssom": { + "erro": "IndexError: index 8 is out of bounds for dimension 0 with size 8" + } + }, + "roteador": { + "amostras": 1588, + "pronto": true, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.053734, + "conf_media": 0.4029, + "frac_rotas_ativas": 0.5938, + "k": 24, + "taxa_ativos": 1.0 + }, + "invariante_sem_orfaos": true, + "orfaos_por_variante": { + "som": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "gg": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "gcs": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "gsom": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "hsom": { + "orfaos": 0, + "relocados": 0 + }, + "tkm": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "rsom": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "cpn": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "ssom": { + "orfaos": 0, + "relocados": 23, + "taxa_ativos": 1.0 + } + } + }, + "avaliacao_final": { + "vqa": 1.9003024697303772, + "pontuacao": 2.7808186411857605, + "instrucao": 1.9354261755943298, + "tts": 1.8977428078651428, + "lm": 2.717095136642456, + "imagem_caption": 2.3393216133117676, + "ocr": 2.880362868309021, + "noticia": 2.6000683307647705, + "asr": 2.036989539861679, + "ppl_lm": 15.136289468671222 + }, + "metricas_completas": { + "neuronios_ativos": { + "ativos/som": 1.0, + "k/som": 24, + "ativos/gg": 1.0, + "k/gg": 4, + "ativos/gcs": 1.0, + "k/gcs": 3, + "ativos/gsom": 1.0, + "k/gsom": 33, + "ativos/hsom": 1.0, + "k/hsom": 12, + "ativos/tkm": 1.0, + "k/tkm": 32, + "ativos/rsom": 1.0, + "k/rsom": 32, + "ativos/cpn": 1.0, + "k/cpn": 24, + "ativos/ssom": 1.0, + "k/ssom": 24, + "A_global": 1.0 + }, + "estruturais": { + "ortogonalidade": 8.344650268554688e-07, + "balanceamento_moe": 0.0, + "gate_global": 0.33352139592170715, + "gate_janela": 0.3531237244606018, + "gate_linear": 0.31335487961769104, + "moe1/experts_ativos": 6.0, + "moe1/uso_max": 0.18867573142051697, + "moe1/perda_lb": 0.05620919540524483, + "moe1/perda_ort": 0.04630301520228386, + "moe1/fase": 0.0, + "moe1/beta_feedback": 0.0, + "moe1/entropia_atn": 0.0, + "moe2/experts_ativos": 6.0, + "moe2/uso_max": 0.1919080913066864, + "moe2/perda_lb": 0.05697876214981079, + "moe2/perda_ort": 0.017570793628692627, + "moe2/fase": 0.0, + "moe2/beta_feedback": -0.08475486189126968, + "moe2/entropia_atn": 0.5860676169395447, + "moe2/alpha_refino": -0.004548724740743637, + "mtp/alpha0": 0.9997836947441101, + "mtp/alpha1": 0.00021627375099342316, + "mtp/termos_ce": 44.0, + "micra0/n_ramos": 3, + "micra0/ramos_ativos": 3, + "micra0/passos_rec": 2, + "micra0/alpha0": 0.1318451315164566, + "micra0/alpha1": 0.3106174170970917, + "micra0/alpha2": 0.5575374364852905, + "micra0/uso0": 0.11339543014764786, + "micra0/uso1": 0.2821197211742401, + "micra0/uso2": 0.6044845581054688, + "micra1/n_ramos": 4, + "micra1/ramos_ativos": 4, + "micra1/passos_rec": 2, + "micra1/alpha0": 0.6494265198707581, + "micra1/alpha1": 0.20311686396598816, + "micra1/alpha2": 0.023890359327197075, + "micra1/alpha3": 0.12356625497341156, + "micra1/uso0": 0.4623371362686157, + "micra1/uso1": 0.23822982609272003, + "micra1/uso2": 0.11237825453281403, + "micra1/uso3": 0.18705463409423828, + "micra2/n_ramos": 4, + "micra2/ramos_ativos": 4, + "micra2/passos_rec": 2, + "micra2/alpha0": 0.9809788465499878, + "micra2/alpha1": 0.01534233894199133, + "micra2/alpha2": 0.0006197995971888304, + "micra2/alpha3": 0.0030591192189604044, + "micra2/uso0": 0.6023621559143066, + "micra2/uso1": 0.11406103521585464, + "micra2/uso2": 0.15544579923152924, + "micra2/uso3": 0.1281307488679886, + "nlp/gate_medio": 0.444911390542984, + "nlp/intencao_id": 6, + "nlg/coerencia_bigramas_vistos": 0.0, + "nlg/estilo_medio": 0.5, + "nlg/estilo_disp": 0.0, + "janela1m/n_segmentos": 0, + "janela1m/m_max": 7813, + "janela1m/hit_celula": 0.0, + "janela1m/consultas": 0, + "computo/computo_C": 0.177, + "computo/nucleos": 2, + "computo/ram_livre_mb": 1246.5, + "computo/n_ramos": 2, + "computo/d_ramo": 64, + "computo/k_rec": 1, + "mtp/rascunho_pronto": 1.0, + "roteador/amostras": 1588, + "roteador/pronto": 1.0, + "roteador/frac_rotas": 0.0, + "roteador/conf_media": 0.2593, + "roteador/drift": 0.053734 + }, + "escala": { + "computo_C": 0.177, + "nucleos": 2, + "ram_livre_mb": 1246.5, + "n_ramos": 2, + "d_ramo": 64, + "k_rec": 1 + }, + "regime": { + "fase_final": "B", + "alvo_ativos": 0.9 + } + }, + "rpp": { + "rho": 1.0, + "streak": 0, + "recompensas": 0, + "punicoes": 2, + "penalidades": 104, + "kappa_rotas_mortas": 0.01 + }, + "roteador_ssom": { + "amostras": 1588, + "pronto": true, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.053734, + "conf_media": 0.2593, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 1.0 + }, + "inferencia_agente": { + "latencia_s": 0.459, + "tokens": 24, + "taxa_repeticao": 0.6667, + "coerencia_bigramas": 0.0 + }, + "difusao": null, + "agente_engenheiro": { + "aprovadas": 0, + "bloqueadas": 0, + "invariante_sem_orfaos": true + }, + "checkpoints_eventos": [ + "checkpoint epoca-007-p03088: 435 tensores, 23 pulados, 94 novos", + "1 checkpoint(s) antigo(s) apagado(s)", + "checkpoint epoca-007-p03150: local (59.2 MB)", + "1 checkpoint(s) antigo(s) apagado(s)", + "checkpoint epoca-007: local (59.2 MB)", + "1 checkpoint(s) antigo(s) apagado(s)", + "checkpoint fase-dpo: local (59.2 MB)", + "1 checkpoint(s) antigo(s) apagado(s)", + "checkpoint fase-som: local (59.2 MB)" + ], + "ram": { + "amostras": 6059, + "rss_min_mb": 542.6, + "rss_med_mb": 2692.2, + "rss_max_mb": 3579.5, + "disponivel_min_mb": 138.6, + "duracao_s": 12836.7 + } +} \ No newline at end of file diff --git a/resumo_treino_v8.json b/resumo_treino_v8.json new file mode 100644 index 0000000000000000000000000000000000000000..11c8eea638c0e0e8601c7d0191471171ee045f89 --- /dev/null +++ b/resumo_treino_v8.json @@ -0,0 +1,974 @@ +{ + "versao": "v8-khtst-melhorias-a-j", + "teto_horas": 5.0, + "tempo_acumulado_h": 3.98, + "memoria_v8": { + "rss_mb": 2808.0, + "pico_rss_mb": 2901.1, + "limiar_mb": 3535.6, + "alpha": 0.85, + "limpezas": 0, + "observacoes": 90 + }, + "autoajuste_v8": null, + "epocas": [ + { + "epoca": 0, + "perda_media": 7.563339779175908, + "avaliacao": { + "vqa": 7.203276952107747, + "pontuacao": 7.940507570902507, + "instrucao": 7.06501563390096, + "tts": 6.899012962977092, + "lm": 8.063114325205484, + "imagem_caption": 6.979289611180623, + "ocr": 8.369198322296143, + "noticia": 7.745315869649251, + "asr": 7.773949146270752, + "ppl_lm": 3175.163243261297 + }, + "eq_som": 0.3644367456436157, + "punicoes": 2, + "prs": { + "trust": 0.5754, + "tau": 7.08859, + "clip": 2.2575, + "boost": 1.0, + "streak": 0, + "ciclos_sgdr": 0, + "recompensa_acum": 15.603, + "beta_min_relativo": 0.575 + }, + "rpp": { + "rho": 1.0, + "streak": 0, + "recompensas": 14, + "punicoes": 2, + "penalidades": 83, + "kappa_rotas_mortas": 0.01 + }, + "roteador": { + "amostras": 41, + "pronto": false, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.017941, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 0.0417 + }, + "confianca": { + "h_ema": 0.1964285671710968, + "ece": 0.07530120481927709, + "posterior": { + "media": 0.38823529411764707, + "bernstein": [ + 0.216560596460874, + 0.5599099917744201 + ] + } + }, + "memoria": { + "slots": 17, + "capacidade": 64, + "escritas": 17, + "rejeitadas_confianca": 3, + "hit_rate": 1.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [] + }, + { + "epoca": 1, + "perda_media": 8.527180158174955, + "avaliacao": { + "vqa": 7.8786665598551435, + "pontuacao": 8.415692011515299, + "instrucao": 7.5004353523254395, + "tts": 7.3423952261606855, + "lm": 8.11315369606018, + "imagem_caption": 7.946273724238078, + "ocr": 8.556553681691488, + "noticia": 8.175559520721436, + "asr": 7.956470807393392, + "ppl_lm": 3338.0887647367836 + }, + "eq_som": 0.16573584079742432, + "punicoes": 1, + "prs": { + "trust": 0.7024, + "tau": 9.02434, + "clip": 5.0, + "boost": 1.0, + "streak": 8, + "ciclos_sgdr": 0, + "recompensa_acum": 16.089, + "beta_min_relativo": 0.702 + }, + "rpp": { + "rho": 1.0, + "streak": 0, + "recompensas": 0, + "punicoes": 1, + "penalidades": 13, + "kappa_rotas_mortas": 0.01 + }, + "roteador": { + "amostras": 7, + "pronto": false, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.076635, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 0.125 + }, + "confianca": { + "h_ema": 0.3969818949699402, + "ece": 0.35769230769230775, + "posterior": { + "media": 0.8, + "bernstein": [ + 0.36442949889125675, + 1.0 + ] + } + }, + "memoria": { + "slots": 3, + "capacidade": 64, + "escritas": 3, + "rejeitadas_confianca": 0, + "hit_rate": 0.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [] + }, + { + "epoca": 2, + "perda_media": 7.530729611714681, + "avaliacao": { + "vqa": 6.954700787862142, + "pontuacao": 7.803481101989746, + "instrucao": 6.964048147201538, + "tts": 6.852313915888469, + "lm": 7.867028474807739, + "imagem_caption": 7.145581881205241, + "ocr": 8.153267065684, + "noticia": 7.7516772747039795, + "asr": 7.569636821746826, + "ppl_lm": 2609.7989711637842 + }, + "eq_som": 0.22748778760433197, + "punicoes": 1, + "prs": { + "trust": 0.4023, + "tau": 6.87234, + "clip": 2.4136, + "boost": 1.0, + "streak": 0, + "ciclos_sgdr": 0, + "recompensa_acum": 0.379, + "beta_min_relativo": 2.0 + }, + "rpp": { + "rho": 1.0, + "streak": 0, + "recompensas": 0, + "punicoes": 1, + "penalidades": 15, + "kappa_rotas_mortas": 0.01 + }, + "roteador": { + "amostras": 7, + "pronto": false, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.045073, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 0.0417 + }, + "confianca": { + "h_ema": 0.365193247795105, + "ece": 0.22999999999999998, + "posterior": { + "media": 0.11764705882352941, + "bernstein": [ + 0.0, + 0.46857976498181986 + ] + } + }, + "memoria": { + "slots": 1, + "capacidade": 64, + "escritas": 1, + "rejeitadas_confianca": 3, + "hit_rate": 0.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [] + }, + { + "epoca": 3, + "perda_media": 7.470894780158996, + "avaliacao": { + "vqa": 7.305373986562093, + "pontuacao": 7.8117343584696455, + "instrucao": 6.982861677805583, + "tts": 6.699165344238281, + "lm": 7.789499680201213, + "imagem_caption": 6.952828089396159, + "ocr": 8.299275239308676, + "noticia": 7.658636728922526, + "asr": 7.480801423390706, + "ppl_lm": 2415.108953044255 + }, + "eq_som": 0.31188270449638367, + "punicoes": 2, + "prs": { + "trust": 0.4822, + "tau": 7.0253, + "clip": 1.5271, + "boost": 1.0, + "streak": 0, + "ciclos_sgdr": 0, + "recompensa_acum": 9.739, + "beta_min_relativo": 2.0 + }, + "rpp": { + "rho": 1.0, + "streak": 0, + "recompensas": 0, + "punicoes": 2, + "penalidades": 100, + "kappa_rotas_mortas": 0.01 + }, + "roteador": { + "amostras": 50, + "pronto": false, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.009737, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 0.0417 + }, + "confianca": { + "h_ema": 0.4308484196662903, + "ece": 0.12999999999999998, + "posterior": { + "media": 0.3431372549019608, + "bernstein": [ + 0.19196517099664429, + 0.4943093388072773 + ] + } + }, + "memoria": { + "slots": 22, + "capacidade": 64, + "escritas": 22, + "rejeitadas_confianca": 3, + "hit_rate": 1.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [] + }, + { + "epoca": 4, + "perda_media": 7.043362287374643, + "avaliacao": { + "vqa": 6.859211603800456, + "pontuacao": 7.844064950942993, + "instrucao": 6.841043631235759, + "tts": 6.587692578633626, + "lm": 7.733375469843547, + "imagem_caption": 7.099506139755249, + "ocr": 8.67655897140503, + "noticia": 7.596554199854533, + "asr": 7.473083972930908, + "ppl_lm": 2283.2964058650737 + }, + "eq_som": 0.24100500345230103, + "punicoes": 1, + "prs": { + "trust": 0.417, + "tau": 6.07795, + "clip": 2.2545, + "boost": 1.0, + "streak": 0, + "ciclos_sgdr": 0, + "recompensa_acum": 0.943, + "beta_min_relativo": 2.0 + }, + "rpp": { + "rho": 1.0, + "streak": 0, + "recompensas": 0, + "punicoes": 1, + "penalidades": 13, + "kappa_rotas_mortas": 0.01 + }, + "roteador": { + "amostras": 6, + "pronto": false, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.029232, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 0.0417 + }, + "confianca": { + "h_ema": 0.3634525537490845, + "ece": 0.2961538461538462, + "posterior": { + "media": 0.13333333333333333, + "bernstein": [ + 0.0, + 0.5281164023006646 + ] + } + }, + "memoria": { + "slots": 1, + "capacidade": 64, + "escritas": 1, + "rejeitadas_confianca": 2, + "hit_rate": 0.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [] + }, + { + "epoca": 5, + "perda_media": 7.089543087537899, + "avaliacao": { + "vqa": 7.19921612739563, + "pontuacao": 7.889442205429077, + "instrucao": 6.684427817662557, + "tts": 6.422936836878459, + "lm": 7.89083456993103, + "imagem_caption": 6.99442187945048, + "ocr": 8.637943903605143, + "noticia": 7.566332896550496, + "asr": 7.405004978179932, + "ppl_lm": 2672.6735231256753 + }, + "eq_som": 0.2562611401081085, + "punicoes": 1, + "prs": { + "trust": 0.6251, + "tau": 6.75615, + "clip": 2.5585, + "boost": 1.0, + "streak": 1, + "ciclos_sgdr": 0, + "recompensa_acum": 14.32, + "beta_min_relativo": 0.625 + }, + "rpp": { + "rho": 1.0375, + "streak": 0, + "recompensas": 5, + "punicoes": 1, + "penalidades": 43, + "kappa_rotas_mortas": 0.01 + }, + "roteador": { + "amostras": 22, + "pronto": false, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.014433, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 0.0417 + }, + "confianca": { + "h_ema": 0.6187677979469299, + "ece": 0.25232558139534883, + "posterior": { + "media": 0.5111111111111111, + "bernstein": [ + 0.25626934934981205, + 0.7659528728724101 + ] + } + }, + "memoria": { + "slots": 11, + "capacidade": 64, + "escritas": 11, + "rejeitadas_confianca": 0, + "hit_rate": 1.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [] + }, + { + "epoca": 6, + "perda_media": 9.40745317524877, + "avaliacao": { + "vqa": 8.110245307286581, + "pontuacao": 7.9338812828063965, + "instrucao": 8.075132290522257, + "tts": 8.336720863978067, + "lm": 8.390085379282633, + "imagem_caption": 8.074243863423666, + "ocr": 8.7576953570048, + "noticia": 7.6731063524882, + "asr": 8.676271120707193, + "ppl_lm": 4403.193619695888 + }, + "eq_som": 0.11337500810623169, + "punicoes": 1, + "prs": { + "trust": 0.6626, + "tau": 9.69135, + "clip": 0.5116, + "boost": 1.0, + "streak": 1, + "ciclos_sgdr": 0, + "recompensa_acum": 22.583, + "beta_min_relativo": 0.663 + }, + "rpp": { + "rho": 1.0, + "streak": 0, + "recompensas": 0, + "punicoes": 1, + "penalidades": 58, + "kappa_rotas_mortas": 0.01 + }, + "roteador": { + "amostras": 29, + "pronto": false, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.002355, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 0.0833 + }, + "confianca": { + "h_ema": 0.5460562705993652, + "ece": 0.20344827586206898, + "posterior": { + "media": 0.35, + "bernstein": [ + 0.1431347476029459, + 0.5568652523970541 + ] + } + }, + "memoria": { + "slots": 13, + "capacidade": 64, + "escritas": 13, + "rejeitadas_confianca": 2, + "hit_rate": 0.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [] + }, + { + "epoca": 7, + "perda_media": 7.231130578782824, + "avaliacao": { + "vqa": 6.5694239139556885, + "pontuacao": 7.822401364644368, + "instrucao": 6.962437709172566, + "tts": 6.983241717020671, + "lm": 7.852646191914876, + "imagem_caption": 7.0820222695668535, + "ocr": 7.846352259318034, + "noticia": 7.663803895314534, + "asr": 7.646427710851033, + "ppl_lm": 2572.532733229914 + }, + "eq_som": 0.3229912519454956, + "punicoes": 1, + "prs": { + "trust": 0.3768, + "tau": 6.82558, + "clip": 5.0, + "boost": 1.0, + "streak": 0, + "ciclos_sgdr": 0, + "recompensa_acum": 9.712, + "beta_min_relativo": 2.0 + }, + "rpp": { + "rho": 1.0, + "streak": 0, + "recompensas": 2, + "punicoes": 1, + "penalidades": 90, + "kappa_rotas_mortas": 0.01 + }, + "roteador": { + "amostras": 45, + "pronto": false, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.009923, + "conf_media": 0.0, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 0.0417 + }, + "confianca": { + "h_ema": 0.398207426071167, + "ece": 0.13666666666666666, + "posterior": { + "media": 0.18478260869565216, + "bernstein": [ + 0.048734518728612175, + 0.32083069866269215 + ] + } + }, + "memoria": { + "slots": 13, + "capacidade": 64, + "escritas": 13, + "rejeitadas_confianca": 10, + "hit_rate": 1.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [] + } + ], + "passos": 3192, + "punicoes": 1, + "barreira_abmo_ativacoes": 0, + "pcgrad_ultimo": { + "pcgrad/dot": 0.0004723769121788418, + "pcgrad/projetado": 0.0, + "pcgrad/fator": 0.0 + }, + "prs_v8": { + "trust": 0.3768, + "tau": 6.82558, + "clip": 5.0, + "boost": 1.0, + "streak": 0, + "ciclos_sgdr": 0, + "recompensa_acum": 9.712, + "beta_min_relativo": 2.0 + }, + "confianca": { + "h_ema": 0.398207426071167, + "ece": 0.13666666666666666, + "posterior_media": 0.18478260869565216, + "bernstein": [ + 0.048734518728612175, + 0.32083069866269215 + ] + }, + "memoria": { + "slots": 13, + "capacidade": 64, + "escritas": 13, + "rejeitadas_confianca": 10, + "hit_rate": 1.0, + "comprimidos": 0, + "entropia_codebook": 1.4736544073912228e-07, + "perda_vq": null, + "violacoes_contrato": 0, + "economia_bits_por_slot": 0.9990234375 + }, + "crescimento": [], + "microunidades": [ + { + "ramos": [ + "conv_dil", + "gru", + "mlp" + ], + "ativos": [ + 1.0, + 1.0, + 1.0 + ], + "uso_ema": [ + 0.9752992987632751, + 0.024680670350790024, + 1.9912109564756975e-05 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "ativos": [ + 1.0, + 1.0, + 1.0, + 1.0 + ], + "uso_ema": [ + 0.12329785525798798, + 0.4805801510810852, + 0.07674114406108856, + 0.31938061118125916 + ], + "passos_rec": 2 + }, + { + "ramos": [ + "conv_dil", + "gru", + "mlp", + "moe" + ], + "ativos": [ + 1.0, + 1.0, + 1.0, + 1.0 + ], + "uso_ema": [ + 0.9999496936798096, + 2.324265779840573e-11, + 4.997722862754017e-05, + 2.525876396930471e-08 + ], + "passos_rec": 2 + } + ], + "dpo": { + "passos": 93, + "acc_final": 1.0, + "margem_final": 14.226043701171875, + "beta_final": 0.05858517438173294 + }, + "som": { + "variante_ativa": "cpn", + "variantes": { + "som": { + "k": 24, + "taxa_ativos": 1.0, + "resemeados": 24, + "usos": 256, + "atingiu_alvo": true + }, + "gg": { + "k": 4, + "taxa_ativos": 1.0, + "resemeados": 4, + "usos": 256, + "atingiu_alvo": true + }, + "gcs": { + "k": 3, + "arestas": 3, + "eq_treino": 4104.4892578125, + "atingiu_alvo": true + }, + "gsom": { + "k": 15, + "gt": 1.05, + "erro_medio": 0.18949279189109802, + "atingiu_alvo": true + }, + "hsom": {}, + "tkm": { + "k": 32, + "taxa_ativos": 1.0, + "resemeados": 32, + "usos": 256, + "atingiu_alvo": true + }, + "rsom": { + "k": 32, + "taxa_ativos": 1.0, + "resemeados": 32, + "usos": 256, + "atingiu_alvo": true + }, + "cpn": {}, + "ssom": { + "erro": "IndexError: index 8 is out of bounds for dimension 0 with size 8" + } + }, + "roteador": { + "amostras": 1581, + "pronto": true, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.009923, + "conf_media": 0.4107, + "frac_rotas_ativas": 0.7188, + "k": 24, + "taxa_ativos": 1.0 + }, + "invariante_sem_orfaos": true, + "orfaos_por_variante": { + "som": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "gg": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "gcs": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "gsom": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "hsom": { + "orfaos": 0, + "relocados": 0 + }, + "tkm": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "rsom": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "cpn": { + "orfaos": 0, + "relocados": 0, + "taxa_ativos": 1.0 + }, + "ssom": { + "orfaos": 0, + "relocados": 23, + "taxa_ativos": 1.0 + } + } + }, + "avaliacao_final": { + "vqa": 6.4591522216796875, + "pontuacao": 7.7690675258636475, + "instrucao": 7.062118411064148, + "tts": 6.546424508094788, + "lm": 7.9568891525268555, + "imagem_caption": 7.035338521003723, + "ocr": 8.0436851978302, + "noticia": 7.5813761949539185, + "asr": 7.698689699172974, + "ppl_lm": 2855.177102389719 + }, + "metricas_completas": { + "neuronios_ativos": { + "ativos/som": 1.0, + "k/som": 24, + "ativos/gg": 1.0, + "k/gg": 4, + "ativos/gcs": 1.0, + "k/gcs": 3, + "ativos/gsom": 1.0, + "k/gsom": 15, + "ativos/hsom": 1.0, + "k/hsom": 12, + "ativos/tkm": 1.0, + "k/tkm": 32, + "ativos/rsom": 1.0, + "k/rsom": 32, + "ativos/cpn": 1.0, + "k/cpn": 24, + "ativos/ssom": 1.0, + "k/ssom": 24, + "A_global": 1.0 + }, + "estruturais": { + "ortogonalidade": 7.152557373046875e-07, + "balanceamento_moe": 0.0, + "gate_global": 0.3491619825363159, + "gate_janela": 0.3269121050834656, + "gate_linear": 0.3239259123802185, + "moe1/experts_ativos": 6.0, + "moe1/uso_max": 0.17838352918624878, + "moe1/perda_lb": 0.055772364139556885, + "moe1/perda_ort": 0.00034026303910650313, + "moe1/fase": 0.0, + "moe1/beta_feedback": 0.0, + "moe1/entropia_atn": 0.0, + "moe2/enc_uso_medio": 0.5, + "moe2/dec_uso_medio": 0.24999994039535522, + "moe2/dec_uso_min": 0.001363456016406417, + "moe2/agrupado": 0.0, + "moe2/perda_lb": 6.0, + "mtp/alpha0": 0.0005300508346408606, + "mtp/alpha1": 0.9994699358940125, + "mtp/termos_ce": 998.0, + "micra0/n_ramos": 3, + "micra0/ramos_ativos": 3, + "micra0/passos_rec": 2, + "micra0/alpha0": 1.0, + "micra0/alpha1": 1.3542193755267107e-10, + "micra0/alpha2": 1.4029073186149995e-15, + "micra0/uso0": 0.9752992987632751, + "micra0/uso1": 0.024680670350790024, + "micra0/uso2": 1.9912109564756975e-05, + "micra1/n_ramos": 4, + "micra1/ramos_ativos": 4, + "micra1/passos_rec": 2, + "micra1/alpha0": 0.07855360209941864, + "micra1/alpha1": 0.5188434720039368, + "micra1/alpha2": 0.07156255841255188, + "micra1/alpha3": 0.3310403823852539, + "micra1/uso0": 0.12329785525798798, + "micra1/uso1": 0.4805801510810852, + "micra1/uso2": 0.07674114406108856, + "micra1/uso3": 0.31938061118125916, + "micra2/n_ramos": 4, + "micra2/ramos_ativos": 4, + "micra2/passos_rec": 2, + "micra2/alpha0": 0.9999498128890991, + "micra2/alpha1": 2.1000236272161743e-11, + "micra2/alpha2": 5.0150272727478296e-05, + "micra2/alpha3": 2.1534841465609134e-08, + "micra2/uso0": 0.9999496936798096, + "micra2/uso1": 2.324265779840573e-11, + "micra2/uso2": 4.997722862754017e-05, + "micra2/uso3": 2.525876396930471e-08, + "nlp/gate_medio": 0.5649126768112183, + "nlp/intencao_id": 7, + "nlg/coerencia_bigramas_vistos": 0.0, + "nlg/estilo_medio": 0.5, + "nlg/estilo_disp": 0.0, + "janela1m/n_segmentos": 0, + "janela1m/m_max": 2017, + "janela1m/hit_celula": 0.0, + "janela1m/consultas": 0, + "mtp/rascunho_pronto": 1.0, + "roteador/amostras": 1581, + "roteador/pronto": 1.0, + "roteador/frac_rotas": 0.0, + "roteador/conf_media": 0.619, + "roteador/drift": 0.009923 + }, + "escala": {}, + "regime": { + "fase_final": "B", + "alvo_ativos": 0.9 + } + }, + "rpp": { + "rho": 1.0, + "streak": 0, + "recompensas": 2, + "punicoes": 1, + "penalidades": 90, + "kappa_rotas_mortas": 0.01 + }, + "roteador_ssom": { + "amostras": 1581, + "pronto": true, + "lambda_rota": 0.35, + "gamma_empate": 0.15, + "drift_codebook": 0.009923, + "conf_media": 0.619, + "frac_rotas_ativas": 0.0, + "k": 24, + "taxa_ativos": 1.0 + }, + "inferencia_agente": { + "latencia_s": 0.481, + "tokens": 24, + "taxa_repeticao": 0.875, + "coerencia_bigramas": 0.0 + }, + "difusao": { + "modo": "planejado", + "op": "txt2img", + "latencia_s": 0.0, + "prompt_enriquecido": "a floresta amazonica de manha, com detalhes nítidos, com composição harmônica, com iluminação suave", + "gerou_pixels": false, + "estatisticas": { + "modo": "planejado", + "repo": "hf-internal-testing/tiny-stable-diffusion-torch", + "device": "cpu", + "pipelines": [], + "eventos": [ + "pipeline txt2img: ImportError: \nStableDiffusionPipeline requires the transformers library but it was not found in your environment. You can install it with pip: `pip\ninstall transformers`\n" + ] + } + }, + "agente_engenheiro": { + "aprovadas": 0, + "bloqueadas": 0, + "invariante_sem_orfaos": true + }, + "checkpoints_eventos": [ + "checkpoint epoca-007-p03102: 568 tensores, 0 pulados, 93 novos", + "1 checkpoint(s) antigo(s) apagado(s)", + "checkpoint epoca-007-p03150: local (62.0 MB)", + "1 checkpoint(s) antigo(s) apagado(s)", + "checkpoint epoca-007: local (62.0 MB)", + "1 checkpoint(s) antigo(s) apagado(s)", + "checkpoint fase-dpo: local (62.0 MB)", + "1 checkpoint(s) antigo(s) apagado(s)", + "checkpoint fase-som: local (62.0 MB)" + ], + "ram": { + "amostras": 5919, + "rss_min_mb": 428.7, + "rss_med_mb": 2524.4, + "rss_max_mb": 3358.4, + "disponivel_min_mb": 435.4, + "duracao_s": 14340.3 + }, + "tempo_nota": "wall time real do MonitorRAM sobre todos os segmentos; contagem pré-correção incluída" +} \ No newline at end of file diff --git a/scripts/01_verificar_ambiente.py b/scripts/01_verificar_ambiente.py new file mode 100644 index 0000000000000000000000000000000000000000..8cc52680e6a1ee4cf8365793c5ae9f58fd0698f3 --- /dev/null +++ b/scripts/01_verificar_ambiente.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Verificação do ambiente KHTST v2: dependências, RAM/disco, token HF +(PRESENÇA apenas — o valor NUNCA é impresso), módulos da v2 importáveis.""" +import os +import sys + +sys.path.insert(0, "/home/z/my-project/khtst/src") + + +def main(): + print("== KHTST v2 — verificação de ambiente ==") + # dependências + faltando = [] + for mod in ("torch", "datasets", "tokenizers", "huggingface_hub", + "safetensors", "soundfile", "numpy", "PIL", "requests", + "pyarrow"): + try: + m = __import__(mod) + versao = getattr(m, "__version__", "?") + print(f" ✓ {mod} {versao}") + except ImportError: + faltando.append(mod) + print(f" ✗ FALTA: {mod}") + if faltando: + print("Instale:", " ".join(faltando)) + return 1 + # recursos + import shutil + total, _, livre = shutil.disk_usage("/home/z/my-project") + print(f" disco: {livre/2**30:.1f} GB livres de {total/2**30:.1f} GB") + with open("/proc/meminfo") as f: + memkb = int(next(f).split()[1]) + print(f" RAM: {memkb/2**20:.1f} GB total") + # token (presença apenas) + tem_token = bool(os.environ.get("HF_TOKEN")) + print(f" HF_TOKEN presente: {tem_token} (valor nunca é impresso)") + # módulos v2 + from khtst.config import Config + from khtst.nucleo.modelo import KHTSTModel +try: + from khtst.acelerado import status as status_acelerado +except Exception: + status_acelerado = None + from khtst.percepcao.moe import MoEAgrupavel + from khtst.treino.mtp import MTPKHTST + from khtst.treino.dpo import PerdaDPO + from khtst.treino.cirurgia_grad import pcgrad_duas_perdas + from khtst.quanta.quantizacao import SmoothQuantAlpha + from khtst.dados.checkpoints import GestorCheckpoints + cfg = Config() + print(" ✓ módulos v2 importáveis (MoE, MTP, DPO, PCGrad, α-STE, Checkpoints)") + n_params_moe = 3 * cfg.modelo.moe["experts_por_grupo"] * \ + 2 * cfg.modelo.d_modelo * cfg.modelo.moe["d_ff_expert"] + print(f" MoE: {cfg.modelo.moe['n_camadas_moe']} blocos × " + f"{cfg.modelo.moe['n_grupos']} grupos × " + f"{cfg.modelo.moe['experts_por_grupo']} experts " + f"(~{n_params_moe/1e6:.2f}M parâmetros de especialistas)") + print("OK") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/02_coletar_corpus.py b/scripts/02_coletar_corpus.py new file mode 100644 index 0000000000000000000000000000000000000000..5aa67c54e79d48426bb2741fc38720e2f15c5eed --- /dev/null +++ b/scripts/02_coletar_corpus.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Coleta do corpus v2 — UM DATASET POR VEZ, cada fonte em SUBPROCESSO isolado +(estado limpo da biblioteca datasets; RAM devolvida ao SO; erro de uma fonte +nunca contamina a seguinte). Streaming=True (+trust_remote_code onde aceito), +18 fontes, dedup global, multimodal em base64. Cache temporário limpo após +cada fonte.""" +import base64 +import json +import os +import subprocess +import sys + +sys.path.insert(0, "/home/z/my-project/khtst/src") + +from khtst.dados.streaming import StreamingCorpus, limpar_cache_temp + +SAIDA = "/home/z/my-project/khtst/cache_dados/corpus_v2.jsonl" +DIR_PARCIAL = "/home/z/my-project/khtst/cache_dados/parciais" +RUNNER = "/home/z/my-project/khtst/scripts/02a_coletar_fonte.py" + + +def _hash_texto(t: str) -> str: + import hashlib + return hashlib.sha1(t.encode("utf-8", "ignore")).hexdigest()[:16] + + +def main(): + os.makedirs(os.path.dirname(SAIDA), exist_ok=True) + os.makedirs("/home/z/my-project/khtst/telemetria_out", exist_ok=True) + os.makedirs(DIR_PARCIAL, exist_ok=True) + limpar_cache_temp() + plano_total = StreamingCorpus().PLANO + eventos = [] + vistos: set[str] = set() + # v5 — RETOMÁVEL: corpus escrito INCREMENTALMENTE (append por fonte); + # na retomada, recarrega o acumulado e pula fontes já OK (estado persistido) + registros = [] + if os.path.exists(SAIDA): + with open(SAIDA, encoding="utf-8") as f: + for linha in f: + try: + reg = json.loads(linha) + except Exception: + continue + chave = _hash_texto(reg.get("texto") or + ((reg.get("entrada") or "") + "␟" + + (reg.get("saida") or ""))) + if chave not in vistos: + vistos.add(chave) + registros.append(reg) + print(f"retomada: {len(registros)} registros acumulados") + for i, plano in enumerate(plano_total): + fonte = plano["id"] + cam_estado_prev = os.path.join(DIR_PARCIAL, f"fonte_{i:02d}.estado.json") + if os.path.exists(cam_estado_prev): + ev_prev = json.load(open(cam_estado_prev, encoding="utf-8")) + if ev_prev.get("status") == "OK" and ev_prev.get("n", 0) > 0: + eventos.append(ev_prev) + print(f" [SKIP] {fonte} (já coletada: n={ev_prev['n']})") + continue + if plano["via"] == "degradado": + # mensagem determinística gerada pelo próprio módulo de streaming + sub = StreamingCorpus() + list(sub._iterar_fonte(plano)) # registra evento degradado + eventos.extend([vars(ev) for ev in sub.eventos]) + continue + cam_parcial = os.path.join(DIR_PARCIAL, f"fonte_{i:02d}.jsonl") + cam_estado = os.path.join(DIR_PARCIAL, f"fonte_{i:02d}.estado.json") + try: + r = subprocess.run( + [sys.executable, RUNNER, "--indice", str(i), + "--saida", cam_parcial, "--estado", cam_estado], + capture_output=True, text=True, timeout=900, + env={**os.environ, "PYTHONPATH": "/home/z/my-project/khtst/src"}) + r_erro = (r.stderr or "sem resposta")[-180:] + except subprocess.TimeoutExpired: + r_erro = "TIMEOUT 900s (fonte lenta — pulada)" + except Exception as e: # rede/Hub: nunca para o lote + r_erro = f"{type(e).__name__}: {e}"[-180:] + if os.path.exists(cam_estado): + ev = json.load(open(cam_estado, encoding="utf-8")) + else: + ev = {"fonte": fonte, "status": "ERRO", "detalhe": r_erro, "n": 0} + # v5 — dedup/append durável ÚNICO (abaixo); contagem de dedup por leitura + n_dedup = 0 + if os.path.exists(cam_parcial): + with open(cam_parcial, encoding="utf-8") as f: + for linha in f: + try: + reg = json.loads(linha) + except Exception: + continue + if reg.get("texto"): + chave = _hash_texto(reg["texto"]) + else: + chave = _hash_texto((reg.get("entrada") or "") + "␟" + + (reg.get("saida") or "")) + if chave in vistos: + n_dedup += 1 + ev["dedup_descartados"] = n_dedup + eventos.append(ev) + print(f" [{ev['status']}] {fonte} (n={ev['n']}, dedup={n_dedup})" + + (f" {ev['detalhe'][:80]}" if ev.get("detalhe") else "")) + # v5 — append DURÁVEL ao corpus (morte do processo não perde progresso) + novos = 0 + if os.path.exists(cam_parcial) and ev.get("status") in ("OK", "PARCIAL"): + with open(cam_parcial, encoding="utf-8") as f, \ + open(SAIDA, "a", encoding="utf-8") as fa: + for linha in f: + try: + reg = json.loads(linha) + except Exception: + continue + if plano["via"] in ("tts_marcos",): + reg.setdefault("imagem", None) + reg.setdefault("audio", None) + if reg.get("texto"): + chave = _hash_texto(reg["texto"]) + else: + chave = _hash_texto((reg.get("entrada") or "") + "␟" + + (reg.get("saida") or "")) + if chave in vistos: + continue + vistos.add(chave) + if reg.get("imagem") is None: + reg.pop("imagem", None) + if reg.get("audio") is None: + reg.pop("audio", None) + registros.append(reg) + fa.write(json.dumps(reg, ensure_ascii=False) + "\n") + novos += 1 + os.remove(cam_parcial) if os.path.exists(cam_parcial) else None + limpar_cache_temp() # um dataset por vez → disco sob controle + import gc + gc.collect() + # escreve corpus final + n_multimodal = 0 + with open(SAIDA, "w", encoding="utf-8") as f: + for r in registros: + if r.get("imagem") or r.get("audio"): + n_multimodal += 1 + f.write(json.dumps(r, ensure_ascii=False) + "\n") + cont = {} + for r in registros: + cont[r["tarefa"]] = cont.get(r["tarefa"], 0) + 1 + print("== contagem por tarefa ==", cont) + print(f"total: {len(registros)} registros ({n_multimodal} multimodais) → {SAIDA}") + with open("/home/z/my-project/khtst/telemetria_out/eventos_coleta_v2.json", + "w", encoding="utf-8") as f: + json.dump(eventos, f, ensure_ascii=False, indent=2) + limpar_cache_temp() + + +if __name__ == "__main__": + main() diff --git a/scripts/02a_coletar_fonte.py b/scripts/02a_coletar_fonte.py new file mode 100644 index 0000000000000000000000000000000000000000..387ac19013a07fc2bc1b35cf64cae110a1547ce9 --- /dev/null +++ b/scripts/02a_coletar_fonte.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Coleta UMA fonte (subprocesso isolado — estado limpo da biblioteca datasets, +RAM devolvida ao SO ao terminar). Recebe o plano em JSON, escreve registros +convertidos (multimodal em base64) num JSONL parcial.""" +import argparse +import base64 +import json +import sys + +sys.path.insert(0, "/home/z/my-project/khtst/src") + +from khtst.dados.streaming import StreamingCorpus, via_especial + + +def _serializa(r: dict) -> dict: + out = {} + for k, v in r.items(): + if v is None: + continue + if isinstance(v, bytes): + out[k] = base64.b64encode(v).decode("ascii") + else: + out[k] = v + return out + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--indice", required=True, type=int, + help="índice da fonte em StreamingCorpus.PLANO") + ap.add_argument("--saida", required=True) + ap.add_argument("--estado", required=True, help="JSON de evento (status/n)") + args = ap.parse_args() + + corpus = StreamingCorpus() + plano = corpus.PLANO[args.indice] + registros = [] + status, detalhe = "OK", plano.get("via", "") + try: + for ex in corpus._iterar_fonte(plano): + if len(registros) >= plano.get("max", 0): + break + reg = plano["ad"](ex) if plano.get("ad") else \ + (ex if via_especial(plano["via"]) else None) + if reg is None: + continue + reg["fonte"] = plano["id"] + reg.setdefault("meta", {}) + registros.append(reg) + except Exception as e: + status = "ERRO" + detalhe = f"{type(e).__name__}: {str(e)[:180]}" + if corpus.eventos: # erro/PULADO interno da fonte + ev = corpus.eventos[0] + status, detalhe = ev.status, ev.detalhe + with open(args.saida, "w", encoding="utf-8") as f: + for r in registros: + f.write(json.dumps(_serializa(r), ensure_ascii=False) + "\n") + with open(args.estado, "w", encoding="utf-8") as f: + json.dump({"fonte": plano["id"], "status": status, + "detalhe": detalhe, "n": len(registros)}, f, + ensure_ascii=False) + print(f"{status}: {plano['id']} n={len(registros)}") + + +if __name__ == "__main__": + main() diff --git a/scripts/03_treinar_tokenizador.py b/scripts/03_treinar_tokenizador.py new file mode 100644 index 0000000000000000000000000000000000000000..ae3cd6182f30d8cbebdb7ffba21f1e0c70c33fb4 --- /dev/null +++ b/scripts/03_treinar_tokenizador.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Treino do tokenizador BPE (Rust, paralelizável) sobre o corpus v2. +Itera TODOS os campos textuais (texto/entrada/saida) — streaming do arquivo, +RAM O(1).""" +import json +import os +import sys + +sys.path.insert(0, "/home/z/my-project/khtst/src") + +from khtst.config import Config +from khtst.dados.tokenizador import TokenizadorKHTST + +CORPUS = "/home/z/my-project/khtst/cache_dados/corpus_v2.jsonl" +SAIDA = "/home/z/my-project/khtst/cache_dados/tokenizador.json" + + +def iterar_textos(caminho): + with open(caminho, encoding="utf-8") as f: + for linha in f: + try: + r = json.loads(linha) + except Exception: + continue + for campo in ("texto", "entrada", "saida"): + t = r.get(campo) + if isinstance(t, str) and len(t.strip()) >= 10: + yield t.strip() + + +def main(): + cfg = Config() + vocab = cfg.modelo.vocab + tk = TokenizadorKHTST() + stats = tk.treinar(iterar_textos(CORPUS), vocab=vocab, salvar_em=SAIDA) + # teste de ida-e-volta (ByteLevel insere espaço inicial — comparar sem ele) + amostras = list(iterar_textos(CORPUS))[:5] + for s in amostras: + ids = tk.encode(s, tarefa="lm", max_len=256) + dec = tk.decode(ids).strip() + assert dec[:40] == s.strip()[:40] or len(s) < 40, "roundtrip falhou" + print(f"tokenizador: {stats['vocab_size']} tokens (pedido {vocab}) " + f"→ {SAIDA}; roundtrip OK em {len(amostras)} amostras") + + +if __name__ == "__main__": + main() diff --git a/scripts/04_treinar.py b/scripts/04_treinar.py new file mode 100644 index 0000000000000000000000000000000000000000..f810372c8b069cc5bd4e37486aaa90d4df1f70f3 --- /dev/null +++ b/scripts/04_treinar.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Treino v4 — pipeline completo em DUAS FASES (doc 15): + + FASE A — rede aumentada (épocas 0..N-1, sem SOM): + FASE Densa (épocas 0..2): máx conexões, MoE softmax-total, MTP com + K ADAPTATIVO, PCGrad, ABMO, PRS V8, AgenteConfiança, MEMÓRIA INTERNA, + GESTOR DE CRESCIMENTO + AUTO-ESCALA POR COMPUTO (doc 12); unidades + NLP/NLG acopladas (nascem neutras — Teorema 15.1); + FASE Foco (época 3): MoE top-k + máscara + QAT-W8A8 final + (escalas POR GRUPO + correção de viés — doc 08 §§5-6); + FASE DPO: pares (dourado, corrompido) com β adaptativo. + FASE B — consolidação SOM com MÁXIMO de neurônios ativos (eq. 15.1): + 8 variantes SEM neurônios isolados (reseeding garantido), + lr do tronco ×0.1, alvo A ≥ 0.90 (Teorema 15.2). + +Uso (CPU em segmentos): + python3 04_treinar.py --orcamento 1500 # segmento de 25 min + python3 04_treinar.py --orcamento 1500 # retoma do último estado do HF + ... quando 'pipeline completo' termina, roda DPO+SOM automaticamente. +""" +import argparse +import base64 +import json +import os +import sys +import threading +import time + +sys.path.insert(0, "/home/z/my-project/khtst/src") + +import torch + +from khtst.config import Config +from khtst.dados.checkpoints import GestorCheckpoints +from khtst.geracao import GeradorMultimodal +from khtst.qualidade import AgenteEngenheiro +from khtst.dados.tokenizador import TokenizadorKHTST +from khtst.memoria.orquestrador import OrquestradorSOM +from khtst.nucleo.modelo import KHTSTModel +from khtst.telemetria.hub import TelemetryHub +from khtst.treino.treinador import TreinadorExtensao + +CORPUS = "/home/z/my-project/khtst/cache_dados/corpus_v2.jsonl" +TOKENIZADOR = "/home/z/my-project/khtst/cache_dados/tokenizador.json" +RESUMO = "/home/z/my-project/khtst/telemetria_out/resumo_treino_v8.json" +RAM_JSONL = "/home/z/my-project/khtst/telemetria_out/ram_treino_v8.jsonl" + + +class MonitorRAM: + """v7 — observação do CONSUMO DE MEMÓRIA RAM durante o treino inteiro + (pedido explícito do Agente Engenheiro). Amostra VmRSS do processo e + MemAvailable do sistema a cada `intervalo_s` em thread daemon; grava JSONL + (t, passo, rss_mb, disponivel_mb) e resume pico/média/mínimo no fim.""" + + def __init__(self, caminho: str, passo_ref, intervalo_s: float = 2.0): + self.caminho = caminho + self.passo_ref = passo_ref + self.intervalo_s = intervalo_s + self._parar = threading.Event() + self._thread = threading.Thread(target=self._laco, daemon=True) + self.amostras: list[dict] = [] + + @staticmethod + def _rss_mb() -> float: + try: + with open("/proc/self/status", encoding="utf-8") as f: + for linha in f: + if linha.startswith("VmRSS:"): + return float(linha.split()[1]) / 1024.0 + except Exception: + pass + return 0.0 + + @staticmethod + def _disponivel_mb() -> float: + try: + with open("/proc/meminfo", encoding="utf-8") as f: + for linha in f: + if linha.startswith("MemAvailable:"): + return float(linha.split()[1]) / 1024.0 + except Exception: + pass + return 0.0 + + def _laco(self): + # modo "a": segmentos separados do pipeline acumulam no MESMO jsonl + with open(self.caminho, "a", encoding="utf-8") as f: + while not self._parar.is_set(): + amostra = {"t": round(time.time(), 2), + "rss_mb": round(self._rss_mb(), 1), + "disponivel_mb": round(self._disponivel_mb(), 1), + "passo": self.passo_ref()} + f.write(json.dumps(amostra) + "\n") + f.flush() + self._parar.wait(self.intervalo_s) + + def iniciar(self): + self._thread.start() + + def parar(self) -> dict: + """Encerra a thread e resume o jsonl INTEIRO (todos os segmentos).""" + self._parar.set() + self._thread.join(timeout=5) + amostras: list[dict] = [] + try: + with open(self.caminho, encoding="utf-8") as f: + for linha in f: + try: + amostras.append(json.loads(linha)) + except Exception: + continue + except FileNotFoundError: + return {"amostras": 0} + rss = [a["rss_mb"] for a in amostras] + disp = [a["disponivel_mb"] for a in amostras] + if not rss: + return {"amostras": 0} + return {"amostras": len(rss), + "rss_min_mb": round(min(rss), 1), + "rss_med_mb": round(sum(rss) / len(rss), 1), + "rss_max_mb": round(max(rss), 1), + "disponivel_min_mb": round(min(disp), 1), + "duracao_s": round(amostras[-1]["t"] - amostras[0]["t"], 1)} + + +def carregar_registros(caminho: str) -> list[dict]: + registros = [] + with open(caminho, encoding="utf-8") as f: + for linha in f: + try: + r = json.loads(linha) + except Exception: + continue + # multimodal volta a bytes + if isinstance(r.get("imagem"), str): + try: + r["imagem"] = base64.b64decode(r["imagem"]) + except Exception: + r["imagem"] = None + if isinstance(r.get("audio"), str): + try: + r["audio"] = base64.b64decode(r["audio"]) + except Exception: + r["audio"] = None + registros.append(r) + return registros + + + +TEMPO_ACUM = "/home/z/my-project/khtst/telemetria_out/tempo_treino_acumulado.json" + + +def tempo_acumulado_h() -> float: + import json as _json + try: + with open(TEMPO_ACUM) as f: + return float(_json.load(f).get("segundos", 0.0)) / 3600.0 + except Exception: + return 0.0 + + +def registrar_tempo(segundos: float): + import json as _json + total = tempo_acumulado_h() * 3600.0 + max(0.0, segundos) + os.makedirs(os.path.dirname(TEMPO_ACUM), exist_ok=True) + with open(TEMPO_ACUM, "w") as f: + _json.dump({"segundos": total, "horas": total / 3600.0}, f) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--orcamento", type=float, default=None, + help="segundos por segmento (CPU: retomada automática)") + ap.add_argument("--max_passos_epoca", type=int, default=None) + ap.add_argument("--sem_dpo", action="store_true") + ap.add_argument("--sem_som", action="store_true") + ap.add_argument("--sem_epocas", action="store_true", + help="v7: pula o laço de épocas (retomada já completa) e vai " + "direto a DPO→SOM→avaliação→resumo") + ap.add_argument("--sem_difusao", action="store_true", + help="v7: pula o ciclo geracional de difusão (economiza RAM/tempo " + "do segmento final; evidência já validada em v5/v6)") + ap.add_argument("--teto_horas", type=float, default=5.0, + help="v8: TETO RÍGIDO de tempo ACUMULADO de treino (h). " + "Ao atingir, o treino para (requisito: parar se " + "ultrapassar 5 horas).") + ap.add_argument("--semente", type=int, default=None, + help="v7: semente torch p/ REPRODUTIBILIDADE da inicialização " + "(diagnóstico v7: init não semeada + lr 0.003 pode divergir; " + "padrão = cfg.dados.semente)") + args = ap.parse_args() + + # v8 — TETO RÍGIDO (requisito): parar se o treino acumulado passar de 5h + acum = tempo_acumulado_h() + if acum >= args.teto_horas: + print(f"[TETO] treino acumulado {acum:.2f} h ≥ teto {args.teto_horas:.1f} h — " + "PARANDO as tarefas de treino (requisito atendido).") + return + print(f"teto de treino: {acum:.2f}/{args.teto_horas:.1f} h acumuladas") + + cfg = Config().de_arquivo("/home/z/my-project/khtst/configs/base.json") + # v7 — reprodutibilidade: init de pesos, dropout e amostragens torch + torch.manual_seed(args.semente if args.semente is not None + else cfg.dados.semente) + os.makedirs("/home/z/my-project/khtst/telemetria_out", exist_ok=True) + hub = TelemetryHub(cfg.telemetria.arquivo_jsonl) + # v7 — Agente Engenheiro: observação do consumo de RAM durante TODO o treino + ram = MonitorRAM(RAM_JSONL, passo_ref=lambda: 0) + ram.iniciar() + print("monitor RAM: amostrando", RAM_JSONL) + tk = TokenizadorKHTST(TOKENIZADOR) + registros = carregar_registros(CORPUS) + print(f"corpus: {len(registros)} registros") + cont = {} + for r in registros: + cont[r["tarefa"]] = cont.get(r["tarefa"], 0) + 1 + print("tarefas:", cont) + + modelo = KHTSTModel(cfg, usar_multimodal=True) + print(f"modelo: {sum(p.numel() for p in modelo.parameters())/1e6:.2f}M parâmetros") + + checkpoints = GestorCheckpoints(dir_local=cfg.checkpoints.dir_local) + orquestrador = OrquestradorSOM(cfg.modelo.d_modelo, cfg.som, hub=hub, + cfg_atencao=vars(cfg.som_atencao) if hasattr(cfg, "som_atencao") else None) + treinar = TreinadorExtensao(cfg, modelo, tk, hub, registros, + orquestrador_som=orquestrador, checkpoints=checkpoints) + # v5 — Agente Engenheiro: observação integral durante o treino (doc 16 §9) + agente = AgenteEngenheiro( + tolerancia_regressao=cfg.agente_engenheiro.tolerancia_regressao, hub=hub) + treinar.agente_engenheiro = agente + treinar.intervalo_agente = cfg.agente_engenheiro.intervalo + # passo global do treino visível ao monitor de RAM + ram.passo_ref = lambda: treinar.passo_global + # v5 — gerador multimodal (difusão) para o ciclo geracional (doc 17) + difusao = GeradorMultimodal(repo_id=cfg.difusao.repo_id, + altura=cfg.difusao.altura, largura=cfg.difusao.largura, + passos_inferencia=cfg.difusao.passos_inferencia, + guia_escala=cfg.difusao.guia_escala, hub=hub) \ + if cfg.difusao.ativo and not args.sem_difusao else None + + # item c: "salvar (e usar) estados" — retoma do checkpoint local mais avançado + tag_retomada = treinar.retomar_ultimo_checkpoint() + print(f"retomada do Hub: tag={tag_retomada} " + f"(passo {treinar.passo_global}, época {treinar.epoca})") + + # ---------- fases 1+2: denso → foco (segmentos com retomada) ---------- + if not args.sem_epocas: + while True: + resultado = treinar.treinar("/home/z/my-project/khtst/checkpoints", + max_passos_por_epoca=args.max_passos_epoca, + orcamento_s=args.orcamento) + print(f"segmento: {resultado['duracao_s']}s | passos={treinar.passo_global} " + f"| épocas={len(treinar.historico_epocas)} | interrompido={resultado['interrompido']}") + registrar_tempo(resultado.get("duracao_s", 0.0)) + print(f"→ tempo acumulado de treino: {tempo_acumulado_h():.2f} h " + f"(teto {args.teto_horas:.1f} h)") + if not resultado["interrompido"]: + break + print("→ estado salvo no Hub; rode o script novamente para o próximo segmento") + if args.orcamento is not None: + return # sai: próximo segmento em nova invocação + return # sem orçamento: uma passada só por chamada + + # ---------- fase 3: DPO ---------- + resultado_dpo = {"pulado": True} + if not args.sem_dpo: + print("== fase DPO ==") + resultado_dpo = treinar.treinar_dpo() + print("DPO:", resultado_dpo) + treinar.salvar_checkpoint("fase-dpo", {"dpo": resultado_dpo}) + + # ---------- fase 4: SOM com máximo de neurônios ativos ---------- + resultado_som = {"pulado": True} + if not args.sem_som: + print("== fase SOM (máx neurônios ativos) ==") + resultado_som = treinar.consolidar_som_max_ativos() + for nome, st in resultado_som.get("variantes", {}).items(): + print(f" {nome}: {st}") + treinar.salvar_checkpoint("fase-som", {"som": { + k: v for k, v in resultado_som.items() if k != "prototipos"}}) + + aval_final = treinar.avaliar(max_lotes=4) + print("avaliação final:", aval_final) + + # v5 — evidência de INFERÊNCIA para o Agente Engenheiro (métricas de geração) + try: + ids_prompt = tk.encode("Pergunta: o que e o Kohonen? Resposta:", + tarefa="instrucao", max_len=24) + t0 = time.time() + tokens = modelo.gerar(torch.tensor([ids_prompt]), max_novos=24) + evid_inf = agente.observar_inferencia(modelo, torch.tensor([ids_prompt]), + tokens, time.time() - t0) + print("inferência (agente):", evid_inf) + print("texto gerado:", tk.decode(tokens)) + except Exception as e: + evid_inf = {"erro": str(e)} + + # v5 — ciclo de compreensão geracional (difusão; modo honesto) + evid_difusao = None + if difusao is not None: + try: + r = difusao.compreensao_geracional( + "a floresta amazonica de manha", modelo, + orquestrador=orquestrador, op="txt2img") + evid_difusao = {"modo": r.get("modo"), "op": r.get("op"), + "latencia_s": r.get("latencia_s"), + "prompt_enriquecido": r.get("prompt_enriquecido"), + "gerou_pixels": r.get("modo") == "real", + "estatisticas": difusao.estatisticas()} + print("difusão (modo honesto):", evid_difusao["modo"], + "—", evid_difusao["prompt_enriquecido"]) + except Exception as e: + evid_difusao = {"erro": str(e)} + + # v4 — MÉTRICAS COMPLETAS (doc 15 §3): neurônios ativos por variante, + # NLP/NLG, janela 1M, computo, crescimento + metricas_completas = { + "neuronios_ativos": (orquestrador.telemetria_ativos() + if orquestrador is not None else {}), + "estruturais": modelo.metricas_estruturais(), + "escala": modelo.info_escalacao, + "regime": {"fase_final": treinar.regime.fase, + "alvo_ativos": treinar.regime.alvo_ativos}, + } + print("== métricas completas (doc 15 §3) ==") + print(json.dumps(metricas_completas, ensure_ascii=False, indent=1, + default=str)[:2200]) + + rel_agente = agente.relatorio(orquestrador_som=orquestrador, + avaliacao=aval_final) + os.makedirs(os.path.dirname(RESUMO), exist_ok=True) + ram_resumo = ram.parar() # resume TODOS os segmentos (jsonl completo) + with open(RESUMO, "w", encoding="utf-8") as f: + registrar_tempo(ram_resumo["duracao_s"] if isinstance(ram_resumo, dict) else 0.0) + json.dump({"versao": "v8-khtst-melhorias-a-j", + "teto_horas": args.teto_horas, + "tempo_acumulado_h": tempo_acumulado_h(), + "memoria_v8": treinar.gestor_memoria.estatisticas(), + "autoajuste_v8": getattr(treinar, "ultimo_autoajuste", None), + "epocas": treinar.historico_epocas, + "passos": treinar.passo_global, + "punicoes": len(treinar.eventos_punicao), + "barreira_abmo_ativacoes": treinar.barreira_ativa_count, + "pcgrad_ultimo": treinar.ultimos_pcgrad, + "prs_v8": treinar.prs.estado(), + "confianca": {"h_ema": treinar.agente.ultimo_h, + "ece": treinar.agente.ece(), + "posterior_media": treinar.agente.posterior.media, + "bernstein": treinar.agente.posterior.bernstein()}, + "memoria": (treinar.memoria.estatisticas() + if treinar.memoria is not None else None), + "crescimento": (treinar.gestor.eventos + if treinar.gestor is not None else []), + "microunidades": [ + {"ramos": c.nome_ramos, + "ativos": [float(a) for a in c.ramo_ativo.tolist()], + "uso_ema": (c.uso_ema.tolist() + if c.uso_ema is not None else None), + "passos_rec": c.ultimo_passos_rec} + for c in modelo.compositores], + "dpo": resultado_dpo, "som": resultado_som, + "avaliacao_final": aval_final, + "metricas_completas": metricas_completas, + "rpp": (treinar.rpp.estado() if treinar.rpp else None), + "roteador_ssom": (treinar.roteador.estatisticas() + if treinar.roteador is not None else None), + "inferencia_agente": evid_inf, + "difusao": evid_difusao, + "agente_engenheiro": {"aprovadas": rel_agente["revisoes_aprovadas"], + "bloqueadas": rel_agente["revisoes_bloqueadas"], + "invariante_sem_orfaos": rel_agente.get("som_invariante_sem_orfaos")}, + "checkpoints_eventos": checkpoints.eventos[-12:], + "ram": ram_resumo}, + f, ensure_ascii=False, indent=2, default=str) + print(f"resumo → {RESUMO}") + print("ram:", json.dumps(ram_resumo)) + + +if __name__ == "__main__": + main() diff --git a/scripts/05_avaliar.py b/scripts/05_avaliar.py new file mode 100644 index 0000000000000000000000000000000000000000..10a12ca0a84f25d21aaaa19242cecfa6b5c27db6 --- /dev/null +++ b/scripts/05_avaliar.py @@ -0,0 +1,654 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Avaliação v3 — relatório de QUALIDADE DO APRENDIZADO com evidências: + + 1. perplexidade LM: modelo inicial (aleatório) vs treinado vs por época; + 2. perdas por tarefa (9 tarefas) treinado vs aleatório; + 3. MoE agrupável (roteamento POR TOKEN causal): top experts POR TAREFA; + 4. MTP adaptativo: perdas por cabeça, α e ECONOMIA de termos de CE; + 5. W8A8: delta de perda com/sem quantização na lm_head; + 6. SOM: taxa de neurônios ativos, EQ, TE por variante; + 7. geração: amostras com/sem punição dinâmica de repetição; + 8. MICROUNIDADES: ramos ativos, gating α por tarefa, refino recursivo; + 9. CONFIANÇA: posterior Beta, cota de Bernstein, ECE de calibração; +10. MEMÓRIA INTERNA: hit-rate, escritas conficientes, compressão VQ; +11. CRESCIMENTO: eventos de expansão/poda das microunidades. +Salva telemetria_out/avaliacao_v5.json (v5: + Agente Engenheiro e difusão).""" +import base64 +import json +import math +import os +import random +import sys +import time + +sys.path.insert(0, "/home/z/my-project/khtst/src") + +import torch + +from khtst.config import Config +from khtst.dados.checkpoints import GestorCheckpoints +from khtst.geracao import GeradorMultimodal +from khtst.qualidade import AgenteEngenheiro +from khtst.dados.tokenizador import TokenizadorKHTST +from khtst.memoria.orquestrador import OrquestradorSOM +from khtst.nucleo.modelo import KHTSTModel +from khtst.telemetria.hub import TelemetryHub +from khtst.treino.treinador import (TAREFAS_AUDIO, TAREFAS_IMAGEM, + _audio_para_tensor, _imagem_para_tensor, + TreinadorExtensao) + +CORPUS = "/home/z/my-project/khtst/cache_dados/corpus_v2.jsonl" +TOKENIZADOR = "/home/z/my-project/khtst/cache_dados/tokenizador.json" +RELATORIO = "/home/z/my-project/khtst/telemetria_out/avaliacao_v8.json" + + +def carregar_registros(caminho): + regs = [] + with open(caminho, encoding="utf-8") as f: + for linha in f: + try: + r = json.loads(linha) + except Exception: + continue + if isinstance(r.get("imagem"), str): + r["imagem"] = base64.b64decode(r["imagem"]) + if isinstance(r.get("audio"), str): + r["audio"] = base64.b64decode(r["audio"]) + regs.append(r) + return regs + + +@torch.no_grad() +def perda_lote(modelo, tk, registros, tarefa, n_lotes=6, lote=8): + """Perda CE média da tarefa (multimodais usam o prefixo real treinável-não).""" + pool = [r for r in registros if r["tarefa"] == tarefa] + if not pool: + return None + perdas = [] + for _ in range(n_lotes): + amostras = random.sample(pool, min(lote, len(pool))) + L = modelo.cfg.modelo.comprimento_ctx + seqs, extras = [], [] + for s in amostras: + inp = tk.encode(s.get("entrada") or "", tarefa=tarefa, max_len=L // 2) \ + if tarefa not in ("lm", "noticia") else [1] + saida_fonte = s.get("saida") or s.get("texto") or "" + out = tk.encode(saida_fonte, tarefa=None, + max_len=max(L - len(inp) - 4, 8), com_bos=False) + if len(out) < 4: + continue + seqs.append(inp + out) + extras.append(s) + if not seqs: + continue + Tmax = max(len(s) for s in seqs) + ids = torch.zeros(len(seqs), Tmax, dtype=torch.long) + alvo = torch.full((len(seqs), Tmax), -100, dtype=torch.long) + emb_prefixo = None + for i, (s, seq) in enumerate(zip(extras, seqs)): + ids[i, :len(seq)] = torch.tensor(seq) + if tarefa not in ("lm", "noticia"): + ninp = len(tk.encode(s.get("entrada") or "", tarefa=tarefa, + max_len=L // 2)) + alvo[i, ninp:len(seq)] = ids[i, ninp:len(seq)] + else: + alvo[i, 1:len(seq)] = ids[i, 1:len(seq)] + if modelo.usar_multimodal: + entradas = {} + if tarefa in TAREFAS_IMAGEM: + lado = modelo.cfg.modelo.imagem["resolucao"] + entradas["imagem"] = torch.stack([ + _imagem_para_tensor(s["imagem"], lado) if s.get("imagem") + else torch.zeros(3, lado, lado) for s in extras]) + if tarefa in TAREFAS_AUDIO: + ondas = [_audio_para_tensor(s["audio"]) for s in extras if s.get("audio")] + if ondas: + Nmax = max(o.shape[0] for o in ondas) + pad = torch.zeros(len(ondas), Nmax) + for i, o in enumerate(ondas): + pad[i, :o.shape[0]] = o + entradas["audio"] = pad + if entradas: + emb_prefixo = modelo.codificar_multimodal(entradas) + _, perda = modelo(ids, alvo=alvo, emb_prefixo=emb_prefixo, tarefa=tarefa) + perdas.append(float(perda)) + return sum(perdas) / len(perdas) if perdas else None + + +@torch.no_grad() +def perdas_som_amostra(modelo, tk, registros, orquestrador, n=64): + seqs = [] + for r in registros[:n]: + seqs.append(tk.encode(r.get("texto") or (r.get("saida") or r.get("entrada", "")), + tarefa=r["tarefa"], max_len=64)[:64]) + Tmax = max(len(s) for s in seqs) + ids = torch.zeros(len(seqs), Tmax, dtype=torch.long) + for i, s in enumerate(seqs): + ids[i, :len(s)] = torch.tensor(s) + ctx = modelo.contexto(ids) + out = {} + for nome, v in orquestrador.variantes.items(): + if hasattr(v, "taxa_ativos"): + out[nome] = {"taxa_ativos": v.taxa_ativos(), + "eq": v.eq(ctx) if hasattr(v, "eq") else None} + return out + + +def _salvar(rel: dict): + """Salvamento incremental — evidência parcial sobrevive a cortes de tempo.""" + os.makedirs(os.path.dirname(RELATORIO), exist_ok=True) + with open(RELATORIO, "w", encoding="utf-8") as f: + json.dump(rel, f, ensure_ascii=False, indent=2, default=str) + + +def main(): + cfg = Config().de_arquivo("/home/z/my-project/khtst/configs/base.json") + random.seed(cfg.dados.semente) + torch.manual_seed(cfg.dados.semente) + hub = TelemetryHub(cfg.telemetria.arquivo_jsonl) + tk = TokenizadorKHTST(TOKENIZADOR) + registros = carregar_registros(CORPUS) + + modelo = KHTSTModel(cfg, usar_multimodal=True) + checkpoints = GestorCheckpoints(dir_local=cfg.checkpoints.dir_local) + tag = checkpoints.ultima_tag() + if tag: + checkpoints.carregar(modelo, tag) + print(f"estado carregado: {tag or 'NENHUM (avaliando aleatório!)'}") + orquestrador = OrquestradorSOM(cfg.modelo.d_modelo, cfg.som, hub=hub, + cfg_atencao=vars(cfg.som_atencao) if hasattr(cfg, "som_atencao") else None) + + # v5 — os SOMs são tensors puros (fora do state_dict): re-consolidação + # determinística a partir do modelo treinado (mesmo protocolo do 07 / + # Teorema 16.5) — as evidências de memória refletem o estado TREINADO. + print("== re-consolidação SOM (protocolo doc 16 §4/§5) ==") + relatorio = {"estado": tag} + amostras = random.sample(registros, min(4 * 64, len(registros))) + ctxs = [] + for i in range(0, len(amostras), 64): + pedaco = amostras[i:i + 64] + seqs = [tk.encode(r.get("texto") or (r.get("saida") or r.get("entrada", "")), + tarefa=r["tarefa"], max_len=64)[:64] for r in pedaco] + Tmax = max(len(x) for x in seqs) + ids_l = torch.zeros(len(seqs), Tmax, dtype=torch.long) + for j, x in enumerate(seqs): + ids_l[j, :len(x)] = torch.tensor(x) + ctxs.append(modelo.contexto(ids_l).detach()) + ctx = torch.cat(ctxs, dim=0) + dominante = "instrucao" if any(r["tarefa"] == "instrucao" for r in amostras) else "lm" + orquestrador.escolher({"rotulo_tarefa": dominante, "denso": True}) + orquestrador.treinar_memoria(ctx, epocas=2) + if getattr(orquestrador, "atencao", None) is not None: + inv = orquestrador.atencao.verificar_sem_orfas(orquestrador, dados=ctx, + resemear=True) + print(" invariante zero-órfãos:", inv.pop("_invariante_ok", None)) + # v6 — consolidação do ROTEADOR S-SOM com os MESMOS (ctx, tarefa) — + # contagens empíricas Laplace-suavizadas (doc 18 §1.1, Teorema 18.3) + if getattr(modelo, "roteador_ssom", None) is not None: + from khtst.percepcao.roteador_ssom import LISTA_TAREFAS as _LT + _rot = torch.tensor( + [_LT.index(r["tarefa"]) if r["tarefa"] in _LT else 0 for r in amostras], + dtype=torch.long) + eq_rot = modelo.roteador_ssom.consolidar(ctx, _rot, passos=6) + st_rot = modelo.roteador_ssom.estatisticas() + relatorio["roteador_ssom"] = st_rot + print(f" roteador S-SOM consolidado: eq={eq_rot:.4f} " + f"conf_media={st_rot['conf_media']} frac_rotas={st_rot['frac_rotas_ativas']} " + f"taxa_ativos={st_rot['taxa_ativos']}") + print(" ativos:", json.dumps(orquestrador.telemetria_ativos())[:200]) + + # 1-2) perdas treinado vs aleatório + modelo.eval() + modelo_aleatorio = KHTSTModel(cfg, usar_multimodal=True) + modelo_aleatorio.eval() + print("== perdas por tarefa (treinado vs aleatório) ==") + perdas_tarefa = {} + for tarefa in ("lm", "noticia", "instrucao", "pontuacao", "imagem_caption", + "vqa", "ocr", "asr", "tts"): + p_t = perda_lote(modelo, tk, registros, tarefa) + p_a = perda_lote(modelo_aleatorio, tk, registros, tarefa) + perdas_tarefa[tarefa] = {"treinado": p_t, "aleatorio": p_a} + print(f" {tarefa:16s} treinado={p_t if p_t is None else round(p_t,3)} " + f"aleatório={p_a if p_a is None else round(p_a,3)}") + relatorio["perdas_tarefa"] = perdas_tarefa + _salvar(relatorio) + if perdas_tarefa["lm"]["treinado"]: + relatorio["ppl_lm"] = math.exp(min(perdas_tarefa["lm"]["treinado"], 12)) + relatorio["ppl_lm_aleatorio"] = math.exp(min(perdas_tarefa["lm"]["aleatorio"], 12)) + print(f" ppl treinado={relatorio['ppl_lm']:.1f} " + f"aleatório={relatorio['ppl_lm_aleatorio']:.1f}") + + # 3) MoE foco na tarefa: uso de experts por tarefa + print("== MoE fase foco: top-2 por tarefa (foco na tarefa) ==") + modelo.definir_fase_moe("foco") + moe_uso = {} + for tarefa in ("lm", "vqa", "ocr", "asr", "tts"): + perda_lote(modelo, tk, registros, tarefa, n_lotes=2) + moes = [b.moe for b in modelo.blocos if b.moe is not None] + # v8 — a MoE enc-dec (item g) não tem grupo_de_idx: reporta os + # DECODERS top-2 (4 experts de saída) + uso_tarefa = [] + for m in moes: + if getattr(m, "_tipo_enc_dec", False): + topo = m.ultimo_p.topk(min(2, m.n_dec)).indices.tolist() + uso_tarefa.append({"experts_top": [int(i) for i in topo], + "grupo_top": ["dec" for _ in topo], + "agrupado": bool(m.agrupado)}) + else: + uso_tarefa.append( + {"experts_top": [int(i) for i in m.ultimo_p.topk(2).indices.tolist()], + "grupo_top": [int(m.grupo_de_idx[i]) for i in m.ultimo_p.topk(2).indices]}) + moe_uso[tarefa] = uso_tarefa + for tarefa, uso in moe_uso.items(): + grupos = uso[0]["grupo_top"] if uso else [] + print(f" {tarefa:16s} grupos top: {grupos}") + relatorio["moe_uso_por_tarefa"] = moe_uso + _salvar(relatorio) + + # 4) MTP (uma passada LM para popular os α e perdas por cabeça) + if modelo.mtp is not None: + ids_m, alvo_m = _lote_lm(tk, registros) + _, _ = modelo(ids_m, alvo=alvo_m, tarefa="lm") + modelo.mtp_do_trunk(ids_m, alvo_m, tarefa="lm") + relatorio["mtp"] = modelo.mtp.ultimos + print("== MTP ==" , relatorio["mtp"]) + + # 5) W8A8 delta + ids, alvo = _lote_lm(tk, registros) + _, p_fp = modelo(ids, alvo=alvo, tarefa="lm") + modelo.qat = True + _, p_q = modelo(ids, alvo=alvo, tarefa="lm") + modelo.qat = False + relatorio["w8a8_delta"] = float(p_q - p_fp) + print(f"== W8A8: Δperda = {relatorio['w8a8_delta']:+.5f} ==") + + # 6) SOM — métricas TREINADAS vêm do resumo do treino (o orquestrador + # não é nn.Module: seu estado é consolidado na fase SOM e gravado lá) + resumo_path = "/home/z/my-project/khtst/telemetria_out/resumo_treino_v8.json" + if os.path.exists(resumo_path): + som_treinado = json.load(open(resumo_path)).get("som", {}) + relatorio["som_treinado"] = som_treinado + print("== SOM (fase de consolidação — taxa de neurônios ativos) ==") + for nome, st in som_treinado.get("variantes", {}).items(): + if isinstance(st, dict) and "taxa_ativos" in st: + print(f" {nome}: taxa_ativos={st['taxa_ativos']:.2f} " + f"alvo={st.get('atingiu_alvo')}") + # EQ fresco no espaço atual (referência) + relatorio["som_eq_fresco"] = perdas_som_amostra(modelo, tk, registros, orquestrador) + _salvar(relatorio) + + # 7) geração com/sem punição + prompt = tk.encode("Recomende um filme brasileiro:", tarefa="instrucao", max_len=24) + ids_p = torch.tensor([prompt]) + sem_pun = modelo.gerar(ids_p, max_novos=30, punicao_repeticao=1.0, top_k=0, top_p=0.9) + com_pun = modelo.gerar(ids_p, max_novos=30, punicao_repeticao=1.4, top_k=0, top_p=0.9) + relatorio["geracao"] = { + "sem_punicao": tk.decode(sem_pun), + "com_punicao": tk.decode(com_pun)} + print("== geração (amostra) ==") + print(" sem punição:", relatorio["geracao"]["sem_punicao"][:120]) + print(" com punição:", relatorio["geracao"]["com_punicao"][:120]) + + # 8) MICROUNIDADES (doc 11): ramos, gating α por tarefa, recursão + print("== microunidades (composição serial/paralela/recursiva) ==") + micra = {} + for tarefa in ("lm", "vqa", "asr", "instrucao"): + perda_lote(modelo, tk, registros, tarefa, n_lotes=1) + micra[tarefa] = [ + {"ramos": c.nome_ramos, + "alpha_medio": [round(a, 4) for a in c.ultimo_alpha.tolist()] + if c.ultimo_alpha is not None else None, + "passos_rec": c.ultimo_passos_rec} + for c in modelo.compositores] + for t, ms in micra.items(): + if ms and ms[0]["alpha_medio"]: + print(f" {t:10s} α bloco0 = {ms[0]['alpha_medio']} rec = {ms[0]['passos_rec']}") + relatorio["microunidades"] = micra + + # 9-11) CONFIANÇA / MEMÓRIA / CRESCIMENTO — estado consolidado no resumo + resumo_v3 = json.load(open(resumo_path)) if os.path.exists(resumo_path) else {} + if resumo_v3: + for chave in ("prs_v8", "confianca", "memoria", "crescimento", "microunidades"): + if chave in resumo_v3 and chave not in relatorio: + relatorio[f"treino_{chave}"] = resumo_v3[chave] + if resumo_v3.get("confianca"): + c = resumo_v3["confianca"] + print(f"== confiança: h_ema={c.get('h_ema')}, ECE={c.get('ece')}, " + f"Bernstein={c.get('bernstein')} ==") + if resumo_v3.get("memoria"): + m = resumo_v3["memoria"] + print(f"== memória: hit_rate={m.get('hit_rate')}, escritas={m.get('escritas')}, " + f"rejeitadas={m.get('rejeitadas_confianca')}, " + f"entropia_codebook={m.get('entropia_codebook')} ==") + if resumo_v3.get("crescimento") is not None: + print(f"== crescimento: {len(resumo_v3['crescimento'])} eventos ==") + + # 12) MEMÓRIA INTERNA EM EXECUÇÃO (doc 11 §9): evidência de integração + # real — escrita conficiente dos contextos do corpus, consulta, evicção, + # compressão VQ e contratos, com o modelo JÁ TREINADO + print("== memória interna em execução (contextos reais do modelo) ==") + from khtst.memoria.interna import MemoriaInterna + from khtst.treino.confianca import AgenteConfianca + mi = MemoriaInterna(d=modelo.d, n_slots=32, h_escrita=0.25, + idade_compressao_s=0.0, n_codigos=64) + ag = AgenteConfianca() + amostras_mem = random.sample(registros, min(64, len(registros))) + h_escreveras = 0 + for r in amostras_mem[:48]: + seq = tk.encode(r.get("texto") or (r.get("saida") or r.get("entrada", "")), + tarefa=r["tarefa"], max_len=64)[:64] + if len(seq) < 4: + continue + ids_m = torch.zeros(1, len(seq), dtype=torch.long) + ids_m[0] = torch.tensor(seq) + logits_m, _ = modelo(ids_m, tarefa=r["tarefa"]) + perda_m = modelo.ultima_perda or 6.0 + h_m = float(ag(AgenteConfianca.features_logits(logits_m), + perda_atual=float(perda_m))) + z_m = modelo.contexto(ids_m)[0] + if mi.escrever(z_m, h_m, origem="nucleo", tarefa=r["tarefa"]): + h_escreveras += 1 + consultas_ok = 0 + for r in amostras_mem[48:]: + seq = tk.encode(r.get("texto") or (r.get("saida") or r.get("entrada", "")), + tarefa=r["tarefa"], max_len=64)[:64] + if len(seq) < 4: + continue + ids_m = torch.zeros(1, len(seq), dtype=torch.long) + ids_m[0] = torch.tensor(seq) + z_m = modelo.contexto(ids_m)[0] + rec_m = mi.consultar(z_m, m=3, origem="raciocinio") + consultas_ok += int(rec_m["acerto"]) + n_comp_m = mi.comprimir_antigos(origem="treino") + stats_m = mi.estatisticas() + stats_m["escritas_aceitas"] = h_escreveras + stats_m["consultas_com_acerto"] = consultas_ok + stats_m["comprimidos_agora"] = n_comp_m + relatorio["memoria_execucao"] = stats_m + _salvar(relatorio) + print(f" escritas aceitas={h_escreveras}/48 · consultas com acerto=" + f"{consultas_ok}/16 · comprimidos={n_comp_m} · " + f"entropia_codebook={stats_m['entropia_codebook']:.3f} · " + f"economia_bits/slot={stats_m['economia_bits_por_slot']:.4f} · " + f"violações_contrato={stats_m['violacoes_contrato']}") + + # curva de épocas (telemetria) + resumo_path = "/home/z/my-project/khtst/telemetria_out/resumo_treino_v8.json" + if os.path.exists(resumo_path): + relatorio["curva_epocas"] = [ + {"epoca": e["epoca"], "perda_media": e["perda_media"], + "ppl_lm": e["avaliacao"].get("ppl_lm")} + for e in json.load(open(resumo_path)).get("epocas", [])] + + # v4 (doc 15 §3.5): COERÊNCIA À MEDIDA QUE OS DADOS CRESCEM + print("== coerência (repetição/ancoragem/entropia) por fator de corpus ==") + try: + coer = metricas_coerencia(modelo, tk, registros) + relatorio["coerencia"] = coer + for chave in ("corpus_25pct", "corpus_50pct", "corpus_100pct"): + if chave in coer: + c = coer[chave] + print(f" {chave}: n={c['n_registros']} " + f"repetição={c['repeticao_pct']}% " + f"ancoragem={c['ancoragem_bigramas']} " + f"entropia={c['entropia_logits']}") + for s in coer.get("amostras", []): + print(f" amostra: {s[:100]}") + except Exception as e: + relatorio["coerencia_erro"] = f"{type(e).__name__}: {e}" + print(f" (coerência falhou: {type(e).__name__})") + + # v6 (doc 18 §3): PACOTE COMPLETO DE MÉTRICAS — alinhamento cross-modal, + # geração de texto, benchmarks e SOM expandido (QE/TE/distorção/σ/α/drift/ + # freq de ativação/U-Matrix) + print("== v6: ALINHAMENTO CROSS-MODAL (CLIP Score + ITM + PPL Multimodal) ==") + try: + from khtst.metricas import (avaliar_clip, avaliar_itm, avaliar_todos, + codigos_visuais, metricas_variante, + pacote_completo, ppl_multimodal_texto, + ppl_visual_bigrama, treinar_itm) + from PIL import Image + import io as _io + pares_mm = [] + for r in registros: + if r["tarefa"] in ("imagem_caption", "vqa") and r.get("imagem"): + try: + img = Image.open(_io.BytesIO(r["imagem"])).convert("RGB") + lado = modelo.cfg.modelo.imagem["resolucao"] + import numpy as _np + arr = _np.asarray(img.resize((lado, lado)), + dtype=_np.float32) / 255.0 + tens = torch.from_numpy(arr).permute(2, 0, 1) + pares_mm.append((r.get("entrada") or "Descreva a imagem:", tens)) + except Exception: + continue + if len(pares_mm) >= 32: + break + if pares_mm: + relatorio["clip_score"] = avaliar_clip(modelo, tk, pares_mm, + n_controle=16) + print(" CLIP Score:", relatorio["clip_score"]) + cab, _ = treinar_itm(modelo, tk, pares_mm[:24], epocas=3, lote=8) + relatorio["itm"] = avaliar_itm(cab, modelo, tk, pares_mm[24:]) + print(" ITM:", relatorio["itm"]) + # corrente visual: códigos VQ por imagem → PPL bigrama + codigos = [codigos_visuais(modelo, p[1]) for p in pares_mm[:24]] + relatorio["ppl_visual"] = ppl_visual_bigrama( + codigos, n_codigos=max(2, max(max(c) for c in codigos) + 1 + if codigos and codigos[0] else 2)) + print(" PPL visual (bigrama códigos VQ):", relatorio["ppl_visual"]) + relatorio["ppl_multimodal"] = ppl_multimodal_texto(modelo, tk, registros) + print(" PPL Multimodal (texto sob prefixo visual/áudio):", + relatorio["ppl_multimodal"]) + except Exception as e: + relatorio["alinhamento_erro"] = f"{type(e).__name__}: {e}" + print(" (alinhamento falhou:", relatorio["alinhamento_erro"], ")") + + print("== v6: GERAÇÃO DE TEXTO (CIDEr + BLEU 1-4 + ROUGE-L + METEOR) ==") + try: + from collections import Counter as _Counter + from khtst.metricas import pacote_completo + from khtst.metricas.geracao_texto import tokenizar as gen_tokenizar + pool_cap = [r for r in registros if r["tarefa"] in ("imagem_caption", "vqa") + and (r.get("saida") or "").strip()][:12] + if pool_cap: + df_corpus = _Counter() + for r in pool_cap: + toks = gen_tokenizar(r["saida"]) + for n in range(1, 5): + for i in range(len(toks) - n + 1): + df_corpus[tuple(toks[i:i + n])] += 1 + medias = {} + por_item = [] + for r in pool_cap: + prompt = (r.get("entrada") or "Descreva a imagem:")[:140] + ids_p = torch.tensor([tk.encode(prompt, tarefa=r["tarefa"], + max_len=48)]) + novos = modelo.gerar(ids_p, max_novos=20, temperatura=0.7, + top_p=0.9) + hip = tk.decodificar(novos) + m = pacote_completo(hip, [r["saida"]], df_corpus=df_corpus, + n_docs=len(pool_cap)) + por_item.append({"hipotese": hip[:80], "ref": r["saida"][:60], + "bleu": m["bleu"], "rouge_l": m["rouge_l"], + "meteor": m["meteor"], "cider": m["cider"]}) + for chave in ("bleu", "rouge_l", "meteor", "cider"): + vals = [p[chave] for p in por_item] + medias[chave] = round(sum(vals) / len(vals), 4) + relatorio["geracao_metricas"] = {"medias": medias, + "n": len(por_item), + "amostras": por_item[:4]} + print(" médias:", medias) + except Exception as e: + relatorio["geracao_metricas_erro"] = f"{type(e).__name__}: {e}" + print(" (geração-métricas falhou:", relatorio["geracao_metricas_erro"], ")") + + print("== v6: BENCHMARKS (MMMU / MME / MathVista — proxies PT-BR) ==") + try: + from khtst.metricas import avaliar_todos + relatorio["benchmarks"] = avaliar_todos(modelo, tk, registros, n_max=30) + for k, v in relatorio["benchmarks"].items(): + print(f" {v.get('benchmark')}: n={v.get('n')} " + f"score={v.get('acuracia', v.get('score_mme'))}") + except Exception as e: + relatorio["benchmarks_erro"] = f"{type(e).__name__}: {e}" + print(" (benchmarks falhou:", relatorio["benchmarks_erro"], ")") + + print("== v6: SOM EXPANDIDO (QE/TE/distorção/σ/α/drift/freq/U-Matrix) ==") + try: + from khtst.metricas import metricas_variante + som_exp = {} + probe_x = ctx # contexto da re-consolidação (doc 16 §4) + for nome, v in orquestrador.variantes.items(): + som_exp[nome] = metricas_variante(nome, v, probe_x) + relatorio["som_expandido"] = som_exp + for nome, mv in som_exp.items(): + if "qe" in mv: + print(f" {nome}: QE={mv['qe']} TE={mv['te']} " + f"distorcao={mv['distorcao']} " + f"drift={mv.get('codebook_drift')} " + f"u={mv.get('u_matrix_resumo', {}).get('u_media')}") + except Exception as e: + relatorio["som_expandido_erro"] = f"{type(e).__name__}: {e}" + print(" (som expandido falhou:", relatorio["som_expandido_erro"], ")") + + # v5 — Agente Engenheiro: observação de INFERÊNCIA + relatório final + print("== Agente Engenheiro (inferência + relatório integral) ==") + try: + ag = AgenteEngenheiro( + tolerancia_regressao=cfg.agente_engenheiro.tolerancia_regressao, + hub=hub) + for tarefa, ids_ex in (("instrucao", None),): + pass + _ids = torch.tensor([tk.encode("Pergunta: quem escreveu isso? Resposta:", + tarefa="instrucao", max_len=24)]) + t0 = time.time() + _novos = modelo.gerar(_ids, max_novos=24) + relatorio["inferencia_agente"] = ag.observar_inferencia( + modelo, _ids, _novos, time.time() - t0) + print(" ", relatorio["inferencia_agente"]) + # v5 — ciclo de compreensão geracional (difusão, modo honesto) + if cfg.difusao.ativo: + gd = GeradorMultimodal(repo_id=cfg.difusao.repo_id, + altura=cfg.difusao.altura, + largura=cfg.difusao.largura, + passos_inferencia=cfg.difusao.passos_inferencia, + guia_escala=cfg.difusao.guia_escala, hub=hub) + r = gd.compreensao_geracional("o pantanal ao entardecer", modelo, + orquestrador=orquestrador, op="txt2img") + relatorio["difusao"] = {"modo": r.get("modo"), + "prompt_enriquecido": r.get("prompt_enriquecido"), + "latencia_s": r.get("latencia_s"), + "gerou_pixels": r.get("modo") == "real"} + print(" difusão:", relatorio["difusao"]["modo"], "—", + relatorio["difusao"]["prompt_enriquecido"]) + rel_ag = ag.relatorio(orquestrador_som=orquestrador, + avaliacao={"ppl_lm": relatorio.get("ppl_lm")}) + relatorio["agente_engenheiro"] = { + "revisoes_aprovadas": rel_ag["revisoes_aprovadas"], + "revisoes_bloqueadas": rel_ag["revisoes_bloqueadas"], + "som_invariante_sem_orfaos": rel_ag.get("som_invariante_sem_orfaos"), + "som_ativos": rel_ag.get("som_ativos")} + except Exception as e: + relatorio["agente_erro"] = f"{type(e).__name__}: {e}" + + os.makedirs(os.path.dirname(RELATORIO), exist_ok=True) + with open(RELATORIO, "w", encoding="utf-8") as f: + json.dump(relatorio, f, ensure_ascii=False, indent=2, default=str) + print(f"relatório → {RELATORIO}") + + +def _lote_lm(tk, registros, lote=8, L=192): + pool = [r for r in registros if r["tarefa"] == "lm"] or registros + seqs = [] + for r in random.sample(pool, min(lote, len(pool))): + t = (r.get("texto") or r.get("saida") or r.get("entrada") or "")[:800] + seqs.append(tk.encode(t, tarefa="lm", max_len=L)) + Tmax = max(len(s) for s in seqs) + ids = torch.zeros(len(seqs), Tmax, dtype=torch.long) + alvo = torch.full((len(seqs), Tmax), -100, dtype=torch.long) + for i, s in enumerate(seqs): + ids[i, :len(s)] = torch.tensor(s) + alvo[i, 1:len(s)] = ids[i, 1:len(s)] + return ids, alvo + + +# ---------------- v4 (doc 15 §3.5): COERÊNCIA das respostas ---------------- + +def metricas_coerencia(modelo, tk, registros, rotulos_prompts=None): + """Coerência medida em três dimensões (doc 15 §3.5): + (i) REPETIÇÃO: n-gramas repetidos por 100 tokens gerados; + (ii) ANCORAGEM no corpus: fração de bigramas gerados que existem nos + bigramas do corpus (proxy de coerência de língua); + (iii) ENTROPIA média dos logits (confiança da política). + Avaliada em FATORES de tamanho de corpus — coerência exibida À MEDIDA + QUE OS DADOS CRESCEM (requisito do usuário).""" + prompts = rotulos_prompts or [ + "O Brasil é um país", + "Para fazer um bolo simples você precisa", + "A previsão do tempo para amanhã", + "O melhor jeito de estudar é", + ] + # bigramas de referência do corpus (hash de pares de ids) + corpus_bg: set[int] = set() + pool = [r for r in registros if r.get("texto") or r.get("saida")] or registros + for r in random.sample(pool, min(60, len(pool))): + t = (r.get("texto") or r.get("saida") or r.get("entrada") or "")[:600] + ids = tk.encode(t, tarefa="lm", max_len=96) + for a, b in zip(ids, ids[1:]): + corpus_bg.add(a * 1000003 + b) + resultados = {} + gerados_todos: list[list[int]] = [] + for fator in (0.25, 0.5, 1.0): # subcorpus crescente + n = max(1, int(len(pool) * fator)) + sub = pool[:n] + # bigramas de referência do subcorpus (menor → menos âncoras) + bg_sub: set[int] = set() + for r in random.sample(sub, min(40, len(sub))): + t = (r.get("texto") or r.get("saida") or r.get("entrada") or "")[:600] + ids = tk.encode(t, tarefa="lm", max_len=96) + for a, b in zip(ids, ids[1:]): + bg_sub.add(a * 1000003 + b) + repeticoes, ancoragens, entropias = [], [], [] + for p in prompts: + ids_p = tk.encode(p, tarefa="lm", max_len=24) + t_ids = torch.tensor([ids_p]) + novos = modelo.gerar(t_ids, max_novos=36, temperatura=0.85, + top_p=0.92) + gerados_todos.append(novos) + if not novos: + continue + # (i) repetição: fração de 3-gramas duplicados + trigs = [tuple(novos[i:i + 3]) for i in range(len(novos) - 2)] + rep = 1.0 - (len(set(trigs)) / len(trigs)) if trigs else 0.0 + repeticoes.append(rep * 100.0) + # (ii) ancoragem: bigramas gerados presentes no subcorpus + bigs = [novos[i] * 1000003 + novos[i + 1] + for i in range(len(novos) - 1)] + anc = (sum(1 for g in bigs if g in bg_sub) / len(bigs)) if bigs else 0.0 + ancoragens.append(anc) + # (iii) entropia média dos logits (confiança) + modelo.eval() + with torch.no_grad(): + x = modelo.emb(t_ids) + for bloco in modelo.blocos: + x, _, _ = bloco(x) + h = modelo.norm_f(x) + if modelo.nlp is not None: + h, _ = modelo.nlp(h) + lg = torch.log_softmax(modelo.lm_head(h[:, -1]), dim=-1) + entropias.append(float(-(lg.exp() * lg).sum())) + resultados[f"corpus_{int(fator*100)}pct"] = { + "n_registros": n, + "repeticao_pct": round(sum(repeticoes) / max(1, len(repeticoes)), 2), + "ancoragem_bigramas": round(sum(ancoragens) / max(1, len(ancoragens)), 4), + "entropia_logits": round(sum(entropias) / max(1, len(entropias)), 4), + } + # amostras para o relatório (legibilidade humana) + resultados["amostras"] = [tk.decodificar(g)[:140] for g in gerados_todos[:4]] + return resultados + + +if __name__ == "__main__": + main() diff --git a/scripts/06_publicar_hf.py b/scripts/06_publicar_hf.py new file mode 100644 index 0000000000000000000000000000000000000000..ba018c2336ad7250d9cd482b6ae9a2ba5242530a --- /dev/null +++ b/scripts/06_publicar_hf.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Publica o KHTST v7 no HuggingFace de modo ORGANIZADO e COMPLETO: + + README.md (model card v7 COM GRÁFICOS DE DESEMPENHO) · LICENSE · configs/ · + src/khtst/ · scripts/ · docs/matematica/ (provas 00–18) · tests/ · evidências + de treino/avaliação/comparação · estados/fase-v8/ (pesos finais + meta SHA-256) + +REGRAS DO HUB (escopo v5): publicação em UM ÚNICO COMMIT COMPLETO — +`upload_folder` com `delete_patterns=["*"]` substitui ATOMICAMENTE todo o +conteúdo do repo (arquivos antigos somem no MESMO commit; nunca há uploads +parciais em sequência curta — o Hub bloqueia esse padrão). + +SEGURANÇA: token EXCLUSIVAMENTE da variável HF_TOKEN — nunca gravado em +arquivo, log ou repo. A publicação NUNCA inclui: cache_dados, telemetria_out, +estados_local, stage_publicar, arquivos com o token. + +Verificações de porta do Agente Engenheiro (pré-publicação): + • nenhum segredo no stage (regex hf_[A-Za-z0-9]{30,}); + • nenhum resíduo proibido (bugwatch, estados_hf, __pycache__, .pt). + +Uso: HF_TOKEN=... python3 06_publicar_hf.py [--repo PowerMachine/khtst-multimodal-ptbr] +""" +import argparse +import os +import re +import shutil +import sys + +RAIZ = "/home/z/my-project/khtst" +STAGE = "/home/z/my-project/khtst/stage_publicar" +PASTA_ESTADO = "estados/fase-v8" # ÚNICO estado publicado (pesos finais) +IGNORAR_DIRS = {"__pycache__", "cache_dados", "telemetria_out", + "cache_hub_tmp", ".git", "build", "estados_local", + "estados_local_teste", "stage_publicar", "hf_stage", + "download", "upload"} +IGNORAR_EXT = (".pt", ".pyc", ".pt.tmp") +PADRAO_TOKEN = re.compile(r"hf_[A-Za-z0-9]{30,}") +RESIDUOS_PROIBIDOS = ("bugwatch", "estados_hf") + + +def preparar_stage() -> int: + if os.path.exists(STAGE): + shutil.rmtree(STAGE) + n = 0 + for raiz, dirs, arquivos in os.walk(RAIZ): + dirs[:] = [d for d in dirs if d not in IGNORAR_DIRS] + rel = os.path.relpath(raiz, RAIZ) + alvo_raiz = os.path.join(STAGE, rel) if rel != "." else STAGE + os.makedirs(alvo_raiz, exist_ok=True) + for a in arquivos: + if a.endswith(IGNORAR_EXT) or a.startswith("."): + continue + if any(r in a.lower() for r in RESIDUOS_PROIBIDOS): + print(f" porta do Agente: arquivo proibido ignorado: {a}") + continue + shutil.copy2(os.path.join(raiz, a), os.path.join(alvo_raiz, a)) + n += 1 + # evidências de treino/avaliação (artefatos de 1ª classe, v7) + for nome in ("avaliacao_v8.json", "resumo_treino_v8.json", + "relatorio_agente_engenheiro.json", + "comparacao_treino_v8.json", "test_khtst_v6_pos_treino.txt", + "teste_item_b_v8.json", "test_khtst_v7_pos_treino.txt", + "tempo_treino_acumulado.json"): + fonte = os.path.join(RAIZ, "telemetria_out", nome) + if os.path.exists(fonte): + shutil.copy2(fonte, os.path.join(STAGE, nome)) + n += 1 + # v7 — GRÁFICOS DE DESEMPENHO (publicados APENAS no card HF — README.md) + dir_graf = os.path.join(RAIZ, "graficos") + if os.path.isdir(dir_graf): + os.makedirs(os.path.join(STAGE, "graficos"), exist_ok=True) + for a in sorted(os.listdir(dir_graf)): + if a.endswith(".png"): + shutil.copy2(os.path.join(dir_graf, a), + os.path.join(STAGE, "graficos", a)) + n += 1 + # v8.3 — GATE DE QUALIDADE (regra do usuário): os ESTADOS do modelo só + # entram se a não-regressão vs AURORA for aprovada; gate False ⇒ publica + # APENAS código/docs/evidências (sem pesos, sem alegação de paridade). + import json as _json + gate_ok = None + cmp_path = os.path.join(RAIZ, "telemetria_out", "comparacao_treino_v8.json") + try: + gate_ok = bool(_json.load(open(cmp_path))["nao_regressao"]["aprovado"]) + except Exception: + gate_ok = None + if gate_ok is False: + print("GATE DE QUALIDADE: FALHOU — estados do modelo NÃO publicados " + "(regra: só publicar estados se qualidade ≥ AURORA).") + return n + sys.path.insert(0, os.path.join(RAIZ, "src")) + from khtst.dados.checkpoints import GestorCheckpoints + from khtst.config import Config + gestor = GestorCheckpoints(dir_local=Config().checkpoints.dir_local, + pasta_publicacao=PASTA_ESTADO) + if gestor.preparar_para_publicacao(STAGE): + n += 2 + print(f"estado final incluído: {PASTA_ESTADO}/modelo.safetensors") + else: + print("AVISO: sem checkpoint local — publicando só código/docs") + return n + + +def auditoria_segredos() -> list[str]: + suspeitos = [] + for raiz, dirs, arquivos in os.walk(STAGE): + for a in arquivos: + if not a.endswith((".json", ".md", ".py", ".txt", ".yaml", ".yml")): + continue + caminho = os.path.join(raiz, a) + try: + with open(caminho, encoding="utf-8", errors="ignore") as f: + if PADRAO_TOKEN.search(f.read()): + suspeitos.append(caminho) + except Exception: + pass + return suspeitos + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--repo", default="PowerMachine/khtst-multimodal-ptbr") + args = ap.parse_args() + + token = os.environ.get("HF_TOKEN") + if not token: + print("ERRO: defina HF_TOKEN no ambiente (nunca em arquivo).") + sys.exit(1) + + n = preparar_stage() + print(f"stage: {n} arquivos (sem caches/segredos/resíduos)") + + suspeitos = auditoria_segredos() + if suspeitos: + print("ERRO: possíveis segredos detectados:", suspeitos) + sys.exit(1) + + from huggingface_hub import HfApi + api = HfApi(token=token) + api.create_repo(args.repo, repo_type="model", exist_ok=True, private=False) + + # PUBLICAÇÃO ÚNICA E COMPLETA: delete_patterns=["*"] remove no MESMO commit + # qualquer arquivo antigo — nada de uploads parciais em sequência + api.upload_folder( + folder_path=STAGE, repo_id=args.repo, repo_type="model", + commit_message="KHTST v7 — RETREINO OBSERVADO: estados antigos apagados; " + "treino do zero com monitor de RAM integral (Agente Engenheiro); " + "métricas de tests/test_khtst_v6.py comparadas com o treino " + "anterior; gráficos de desempenho publicados no card", + delete_patterns=["*"], + allow_patterns=["*.py", "*.md", "*.json", "*.yaml", "*.yml", + "*.txt", "*.png", "*.safetensors", "LICENSE*"]) + print("✓ PUBLICAÇÃO ÚNICA COMPLETA (1 commit; antigos removidos " + "atomicamente)") + + # verificação final: resíduos proibidos não podem existir no repo + arquivos = api.list_repo_files(args.repo, repo_type="model") + restos = [f for f in arquivos + if any(r in f.lower() for r in RESIDUOS_PROIBIDOS) + or "__pycache__" in f or f.endswith(".pt")] + if restos: + print("ERRO: resíduos no repo:", restos) + sys.exit(1) + parciais = [f for f in arquivos if f.startswith("estados/") + and "fase-v7" not in f] + print(f"✓ verificação: {len(arquivos)} arquivos no repo; " + f"resíduos=0; estados parciais={len(parciais)} (0 esperado)") + + print(f"REPO: https://huggingface.co/{args.repo}") + print("Higienização: unset HF_TOKEN && python3 -m pip show huggingface_hub " + ">/dev/null && python3 -c 'from huggingface_hub import HfApi; " + "HfApi().logout()'") + + +if __name__ == "__main__": + main() diff --git a/scripts/07_reconsolidar_som.py b/scripts/07_reconsolidar_som.py new file mode 100644 index 0000000000000000000000000000000000000000..0584a63c408a0d3eccb8cbb7d6876a6a8de436c1 --- /dev/null +++ b/scripts/07_reconsolidar_som.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Reconsolidação SOM (FASE B) sobre o estado final do treino v4 — usado após +correções pontuais nas variantes (ex.: reseeding da S-SOM, doc 15 Teorema 15.2) +sem re-executar o treino principal/DPO. Carrega o último estado local, roda +`consolidar_som_max_ativos` e reavalia as métricas de neurônios ativos.""" +import base64 +import json +import os +import sys + +sys.path.insert(0, "/home/z/my-project/khtst/src") + +import torch + +from khtst.config import Config +from khtst.dados.checkpoints import GestorCheckpoints +from khtst.dados.tokenizador import TokenizadorKHTST +from khtst.memoria.orquestrador import OrquestradorSOM +from khtst.nucleo.modelo import KHTSTModel +from khtst.telemetria.hub import TelemetryHub +from khtst.treino.treinador import TreinadorExtensao + +CORPUS = "/home/z/my-project/khtst/cache_dados/corpus_v2.jsonl" +TOKENIZADOR = "/home/z/my-project/khtst/cache_dados/tokenizador.json" +RESUMO = "/home/z/my-project/khtst/telemetria_out/resumo_treino_v7.json" + + +def carregar_registros(caminho: str) -> list[dict]: + registros = [] + with open(caminho, encoding="utf-8") as f: + for linha in f: + try: + r = json.loads(linha) + except Exception: + continue + if isinstance(r.get("imagem"), str): + try: + r["imagem"] = base64.b64decode(r["imagem"]) + except Exception: + r["imagem"] = None + if isinstance(r.get("audio"), str): + try: + r["audio"] = base64.b64decode(r["audio"]) + except Exception: + r["audio"] = None + registros.append(r) + return registros + + +def main(): + cfg = Config().de_arquivo("/home/z/my-project/khtst/configs/base.json") + hub = TelemetryHub(cfg.telemetria.arquivo_jsonl) + tk = TokenizadorKHTST(TOKENIZADOR) + registros = carregar_registros(CORPUS) + print(f"corpus: {len(registros)} registros") + + modelo = KHTSTModel(cfg, usar_multimodal=True) + checkpoints = GestorCheckpoints(dir_local=cfg.checkpoints.dir_local) + tag = checkpoints.ultima_tag() + if tag: + checkpoints.carregar(modelo, tag) + print(f"estado carregado: {tag or 'NENHUM'}") + orquestrador = OrquestradorSOM(cfg.modelo.d_modelo, cfg.som, hub=hub, + cfg_atencao=vars(cfg.som_atencao) if hasattr(cfg, "som_atencao") else None) + treinar = TreinadorExtensao(cfg, modelo, tk, hub, registros, + orquestrador_som=orquestrador, checkpoints=checkpoints) + + print("== FASE B: reconsolidação SOM (S-SOM agora com reseeding) ==") + resultado_som = treinar.consolidar_som_max_ativos() + ativos = orquestrador.telemetria_ativos() + print("neurônios ativos por variante:", json.dumps(ativos, indent=1)) + + aval_final = treinar.avaliar(max_lotes=4) + print("avaliação pós-reconsolidação:", aval_final) + + if os.path.exists(RESUMO): + resumo = json.load(open(RESUMO, encoding="utf-8")) + resumo["som"] = {k: v for k, v in resultado_som.items() + if k != "prototipos"} + resumo["avaliacao_final"] = aval_final + mc = resumo.get("metricas_completas", {}) + mc["neuronios_ativos"] = ativos + resumo["metricas_completas"] = mc + with open(RESUMO, "w", encoding="utf-8") as f: + json.dump(resumo, f, ensure_ascii=False, indent=2, default=str) + print(f"resumo atualizado → {RESUMO}") + checkpoints.salvar(modelo, "fase-som-v7", + {"passo_global": treinar.passo_global, + "reconsolidacao": True, + "ativos": ativos}) + + +if __name__ == "__main__": + main() diff --git a/scripts/08_graficos_card.py b/scripts/08_graficos_card.py new file mode 100644 index 0000000000000000000000000000000000000000..abec58173aee74837bf29f61326ad4aab862e7fc --- /dev/null +++ b/scripts/08_graficos_card.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""v7 — COMPARAÇÃO DE TREINOS + GRÁFICOS DE DESEMPENHO (apenas no card HF). + +Compara o treino ATUAL (resumo_treino_v8.json / avaliacao_v8.json) com o +treino ANTERIOR (*_anterior.json), incorpora as métricas observadas em +tests/test_khtst_v6.py e o monitor de RAM, e produz os gráficos do model +card (graficos/*.png). O Agente Engenheiro valida a NÃO-REGRESSÃO +(Teorema 16.10) das capacidades fundamentais. + +Gráficos (todos em PT-BR, publicados SOMENTE no card HF): + 1. graf_curva_perda.png — perda média por época (anterior vs atual) + 2. graf_ppl_lm.png — ppl LM por época (anterior vs atual, log) + 3. graf_ram.png — consumo de RAM durante o treino (Agente) + 4. graf_perdas_tarefa.png — perda por tarefa (anterior vs atual) + 5. graf_som_roteador.png — SOM: taxa de ativos por variante + RPP + 6. graf_alinhamento.png — CLIP/PPL-multimodal/PPL-visual (honesto) +""" +import json +import math +import os +import sys + +sys.path.insert(0, "/home/z/my-project/khtst/src") + +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +RAIZ = "/home/z/my-project/khtst" +TEL = os.path.join(RAIZ, "telemetria_out") +GRAF = os.path.join(RAIZ, "graficos") + +COR_ANTE = "#7f8fa6" +COR_ATUAL = "#e17055" +COR_AZUL = "#0984e3" +COR_VERDE = "#00b894" + + +def carregar(nome): + p = os.path.join(TEL, nome) + if not os.path.exists(p): + return None + with open(p, encoding="utf-8") as f: + texto = f.read() + if nome.endswith(".jsonl"): + # JSONL: um objeto por linha (monitor de RAM) + amostras = [] + for linha in texto.splitlines(): + try: + amostras.append(json.loads(linha)) + except Exception: + continue + return amostras + return json.loads(texto) + + +def curva_epocas(resumo): + """[(epoca, perda_media, ppl_lm)] por época.""" + pts = [] + for e in (resumo or {}).get("epocas", []): + av = e.get("avaliacao", {}) + pts.append((e.get("epoca", 0), e.get("perda_media"), + av.get("ppl_lm"))) + return pts + + +def parse_testes(nome="test_khtst_v6_pos_treino.txt"): + p = os.path.join(TEL, nome) + ok, fal, cru = None, None, "" + if os.path.exists(p): + cru = open(p, encoding="utf-8", errors="ignore").read() + for linha in cru.splitlines(): + if "resultado:" in linha and "✓" in linha: + partes = linha.split() + try: + ok = int(partes[partes.index("✓") - 1]) + fal = int(partes[partes.index("✗") - 1]) + except Exception: + pass + return {"ok": ok, "falhou": fal, "cru": cru[-400:]} + + +def main(): + os.makedirs(GRAF, exist_ok=True) + ant_r = carregar("resumo_treino_v6_anterior.json") + atu_r = carregar("resumo_treino_v8.json") + ant_a = carregar("avaliacao_v6_anterior.json") + atu_a = carregar("avaliacao_v8.json") + ram = carregar("ram_treino_v8.jsonl") or [] + testes = parse_testes() + + cmp_: dict = { + "versao_atual": (atu_r or {}).get("versao", "v7"), + "versao_anterior": (ant_r or {}).get("versao", "v6"), + "test_khtst_v6": {"ok": testes["ok"], "falhou": testes["falhou"]}, + "ram": (atu_r or {}).get("ram", {}), + } + + # ---------------- 1) curva de perda ---------------- + plt.rcParams.update({"font.size": 10}) + ca, cn = curva_epocas(ant_r), curva_epocas(atu_r) + fig, ax = plt.subplots(figsize=(7.2, 4.2), constrained_layout=True) + if ca: + ax.plot([p[0] for p in ca], [p[1] for p in ca], "o--", color=COR_ANTE, + label="treino anterior (v6)", lw=1.6, ms=5) + if cn: + ax.plot([p[0] for p in cn], [p[1] for p in cn], "o-", color=COR_ATUAL, + label="retreino atual (v7)", lw=2.0, ms=6) + ax.set_xlabel("época") + ax.set_ylabel("perda média (CE ponderada, 9 tarefas)") + ax.set_title("Curva de aprendizado — perda média por época") + ax.grid(alpha=0.3) + ax.legend(loc="upper right", frameon=False) + fig.savefig(os.path.join(GRAF, "graf_curva_perda.png"), dpi=150) + plt.close(fig) + + # ---------------- 2) ppl LM por época ---------------- + fig, ax = plt.subplots(figsize=(7.2, 4.2), constrained_layout=True) + if ca: + ax.plot([p[0] for p in ca], [max(p[2], 1e-1) for p in ca], "o--", + color=COR_ANTE, label="treino anterior (v6)", lw=1.6, ms=5) + if cn: + ax.plot([p[0] for p in cn], [max(p[2], 1e-1) for p in cn], "o-", + color=COR_ATUAL, label="retreino atual (v7)", lw=2.0, ms=6) + aleat = ((ant_a or {}).get("ppl_lm_aleatorio") + or (atu_a or {}).get("ppl_lm_aleatorio")) + if aleat: + ax.axhline(aleat, color="#b2bec3", ls=":", lw=1.4, + label=f"inicialização aleatória (ppl≈{aleat:,.0f})") + ax.set_yscale("log") + ax.set_xlabel("época") + ax.set_ylabel("perplexidade LM (escala log)") + ax.set_title("Perplexidade LM por época") + ax.grid(alpha=0.3, which="both") + ax.legend(loc="lower left", frameon=False, fontsize=9) + fig.savefig(os.path.join(GRAF, "graf_ppl_lm.png"), dpi=150) + plt.close(fig) + + # ---------------- 3) RAM (Agente Engenheiro) ---------------- + if ram: + t0 = ram[0]["t"] + minutos = [(a["t"] - t0) / 60.0 for a in ram] + rss = [a["rss_mb"] for a in ram] + disp = [a["disponivel_mb"] for a in ram] + passos = [a.get("passo", 0) for a in ram] + fig, ax = plt.subplots(figsize=(7.6, 4.4), constrained_layout=True) + ax.plot(minutos, rss, color=COR_AZUL, lw=1.4, + label="RSS do processo (treino)") + mx = max(rss) + md = sum(rss) / len(rss) + ax.axhline(mx, color="#d63031", ls="--", lw=1.1, + label=f"pico {mx:,.0f} MB") + ax.axhline(md, color=COR_VERDE, ls=":", lw=1.1, + label=f"média {md:,.0f} MB") + ax2 = ax.twinx() + ax2.fill_between(minutos, disp, color="#dfe6e9", alpha=0.55, + label="RAM disponível (sistema)") + ax2.set_ylabel("disponível (MB)", color="#636e72") + ax.set_xlabel("tempo de treino (min)") + ax.set_ylabel("RSS do processo (MB)") + ax.set_title("Consumo de memória RAM durante o retreino (Agente Engenheiro)") + ax.set_xlim(0, max(minutos[-1], 0.5)) + ax.grid(alpha=0.3) + # marcações de passos a cada ~25% do treino + p_max = max(passos) or 0 + for frac in (0.25, 0.5, 0.75): + i = min(range(len(passos)), key=lambda j: abs(passos[j] - frac * p_max)) + ax.annotate(f"passo {passos[i]}", (minutos[i], rss[i]), + textcoords="offset points", xytext=(0, 8), + fontsize=7.5, color="#2d3436", ha="center") + h1, l1 = ax.get_legend_handles_labels() + h2, l2 = ax2.get_legend_handles_labels() + ax.legend(h1 + h2, l1 + l2, loc="upper left", + bbox_to_anchor=(0.0, 1.22), ncols=2, frameon=False, fontsize=8.5) + fig.savefig(os.path.join(GRAF, "graf_ram.png"), dpi=150) + plt.close(fig) + + # ---------------- 4) perdas por tarefa ---------------- + def perdas_tarefa(av): + pt = (av or {}).get("perdas_tarefa", {}) + return {k: v.get("treinado") for k, v in pt.items() + if isinstance(v, dict) and v.get("treinado") is not None} + + pa, pn = perdas_tarefa(ant_a), perdas_tarefa(atu_a) + if pa and pn: + tarefas = sorted(set(pa) & set(pn)) + x = range(len(tarefas)) + larg = 0.38 + fig, ax = plt.subplots(figsize=(7.6, 4.2), constrained_layout=True) + ax.bar([i - larg / 2 for i in x], [pa[t] for t in tarefas], larg, + color=COR_ANTE, label="treino anterior (v6)") + ax.bar([i + larg / 2 for i in x], [pn[t] for t in tarefas], larg, + color=COR_ATUAL, label="retreino atual (v7)") + ax.set_xticks(list(x)) + ax.set_xticklabels(tarefas, rotation=28, ha="right") + ax.set_ylabel("perda final por tarefa (treinada)") + ax.set_title("Perda final por tarefa — menor é melhor") + ax.grid(alpha=0.3, axis="y") + ax.legend(loc="upper right", frameon=False) + fig.savefig(os.path.join(GRAF, "graf_perdas_tarefa.png"), dpi=150) + plt.close(fig) + cmp_["perdas_tarefa"] = {t: {"anterior": pa[t], "atual": pn[t]} + for t in tarefas} + + # ---------------- 5) SOM variantes + RPP ---------------- + def variantes(av): + return ((av or {}).get("som_treinado", {}) or {}).get("variantes", {}) + + va, vn = variantes(ant_a), variantes(atu_a) + if va and vn: + nomes = sorted(set(va) & set(vn)) + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8.4, 4.0), + constrained_layout=True) + y = range(len(nomes)) + ax1.barh([i + 0.2 for i in y], [va[n].get("taxa_ativos", 0) for n in nomes], + 0.38, color=COR_ANTE, label="anterior (v6)") + ax1.barh([i - 0.2 for i in y], [vn[n].get("taxa_ativos", 0) for n in nomes], + 0.38, color=COR_ATUAL, label="atual (v7)") + ax1.set_yticks(list(y)) + ax1.set_yticklabels(nomes) + ax1.invert_yaxis() + ax1.set_xlim(0, 1.05) + ax1.axvline(0.90, color="#d63031", ls=":", lw=1.2, + label="alvo 0.90 (Teo. 15.2)") + ax1.set_xlabel("taxa de neurônios ativos") + ax1.set_title("SOM — 8 variantes + S-SOM") + ax1.grid(alpha=0.3, axis="x") + ax1.legend(loc="lower right", fontsize=8, frameon=False) + rpa = ((ant_r or {}).get("rpp") or {}) + rpn = ((atu_r or {}).get("rpp") or {}) + cats = ["recompensas", "punicoes", "penalidades"] + vals_a = [rpa.get(c, 0) for c in cats] + vals_n = [rpn.get(c, 0) for c in cats] + x2 = range(len(cats)) + ax2.bar([i - 0.2 for i in x2], vals_a, 0.38, color=COR_ANTE, + label="anterior (v6)") + ax2.bar([i + 0.2 for i in x2], vals_n, 0.38, color=COR_ATUAL, + label="atual (v7)") + ax2.set_xticks(list(x2)) + ax2.set_xticklabels(["REWARD", "PUNISH", "PENALTY"]) + ax2.set_ylabel("eventos no treino") + ax2.set_title("RPP — Reward/Punishment/Penalty") + ax2.grid(alpha=0.3, axis="y") + ax2.legend(fontsize=8, frameon=False) + fig.savefig(os.path.join(GRAF, "graf_som_roteador.png"), dpi=150) + plt.close(fig) + cmp_["som_taxa_ativos"] = {n: {"anterior": va[n].get("taxa_ativos"), + "atual": vn[n].get("taxa_ativos")} + for n in nomes} + cmp_["rpp"] = {"anterior": rpa, "atual": rpn} + + # ---------------- 6) alinhamento multimodal (honesto) ---------------- + def aln(av): + return {"clip_real": ((av or {}).get("clip_score", {}) or {}).get("clip_real"), + "clip_controle": ((av or {}).get("clip_score", {}) or {}).get("clip_controle"), + "ppl_mm": ((av or {}).get("ppl_multimodal", {}) or {}).get("ppl_mm"), + "ppl_visual": ((av or {}).get("ppl_visual", {}) or {}).get("ppl_visual"), + "itm_acc": ((av or {}).get("itm", {}) or {}).get("acuracia")} + + aa, an = aln(ant_a), aln(atu_a) + if aa and an: + fig, ax = plt.subplots(figsize=(7.6, 4.0), constrained_layout=True) + chaves = ["clip_real", "ppl_mm", "ppl_visual", "itm_acc"] + rotulos = ["CLIP score (real)", "PPL multimodal", "PPL visual", + "ITM acurácia"] + va_ = [aa[k] if isinstance(aa[k], (int, float)) else 0 for k in chaves] + vn_ = [an[k] if isinstance(an[k], (int, float)) else 0 for k in chaves] + x = range(len(chaves)) + ax.bar([i - 0.2 for i in x], va_, 0.38, color=COR_ANTE, + label="anterior (v6)") + ax.bar([i + 0.2 for i in x], vn_, 0.38, color=COR_ATUAL, + label="atual (v7)") + for i, (a_, n_) in enumerate(zip(va_, vn_)): + if isinstance(aa[chaves[i]], (int, float)): + ax.text(i - 0.2, a_, f"{a_:.2f}", ha="center", va="bottom", + fontsize=8) + if isinstance(an[chaves[i]], (int, float)): + ax.text(i + 0.2, n_, f"{n_:.2f}", ha="center", va="bottom", + fontsize=8) + ax.set_xticks(list(x)) + ax.set_xticklabels(rotulos, fontsize=9) + ax.set_title("Alinhamento multimodal (CLIP/ITM/PPL) — leitura honesta") + ax.grid(alpha=0.3, axis="y") + ax.legend(loc="upper right", frameon=False) + fig.savefig(os.path.join(GRAF, "graf_alinhamento.png"), dpi=150) + plt.close(fig) + cmp_["alinhamento"] = {"anterior": aa, "atual": an} + + # ---------------- comparacao + NÃO-REGRESSÃO ---------------- + ppl_a = (ant_a or {}).get("ppl_lm") + ppl_n = (atu_a or {}).get("ppl_lm") + cmp_["ppl_lm"] = {"anterior": ppl_a, "atual": ppl_n, + "delta_pct": (round(100 * (ppl_n - ppl_a) / ppl_a, 2) + if ppl_a and ppl_n else None)} + if ant_r and atu_r and ant_r.get("epocas") and atu_r.get("epocas"): + pm_a = ant_r["epocas"][-1].get("perda_media") + pm_n = atu_r["epocas"][-1].get("perda_media") + cmp_["perda_media_final"] = {"anterior": pm_a, "atual": pm_n} + cmp_["passos"] = {"anterior": ant_r.get("passos"), + "atual": atu_r.get("passos")} + + # Teorema 16.10 — capacidades FUNDAMENTAIS do gate (ppl/perda: menor é + # melhor). ppl_multimodal fica FORA do gate: o codebook VQ é re-estimado + # por treino (k-means por crop) e sua variância entre corridas independen- + # tes é maior que a própria diferença de qualidade — é REPORTADO honesta- + # mente na comparação e nos gráficos, mas não bloqueia a publicação. + _ep_a = (ant_r or {}).get("epocas") or [{}] + caps_antes = {"ppl_lm": ppl_a, + "perda_media_final": _ep_a[-1].get("perda_media")} + caps_depois = {"ppl_lm": ppl_n, + "perda_media_final": (atu_r or {}).get("epocas", [{}])[-1] + .get("perda_media")} + # saúde SOM entra como capacidade (maior/menor conforme a métrica) + _ta_a = [v.get("taxa_ativos") for v in variantes(ant_a).values() + if isinstance(v.get("taxa_ativos"), (int, float))] + _ta_n = [v.get("taxa_ativos") for v in variantes(atu_a).values() + if isinstance(v.get("taxa_ativos"), (int, float))] + if _ta_a: + caps_antes["taxa_ativos_min"] = min(_ta_a) + if _ta_n: + caps_depois["taxa_ativos_min"] = min(_ta_n) + try: + from khtst.config import Config + from khtst.qualidade import AgenteEngenheiro + ag = AgenteEngenheiro(dir_telemetria=TEL, + tolerancia_regressao=Config() + .agente_engenheiro.tolerancia_regressao) + # v8.3 — CORREÇÃO de bug latente do v7: o gate usava as chaves + # "ppl_valid/perda_valid", mas as métricas reais são "ppl_lm" e + # "perda_media_final" — o gate NUNCA verificou o ppl! Com as chaves + # corretas, "menor é melhor" passa a valer de fato (Teorema 16.10). + nr = ag.garantir_nao_regressao( + {k: v for k, v in caps_antes.items() if v is not None}, + {k: v for k, v in caps_depois.items() if v is not None}, + menor_e_melhor=("ppl_valid", "perda_valid", "ppl_lm", + "perda_media_final", "perda_lm")) + cmp_["nao_regressao"] = nr + except Exception as e: + cmp_["nao_regressao"] = {"erro": f"{type(e).__name__}: {e}"} + + with open(os.path.join(TEL, "comparacao_treino_v8.json"), "w", + encoding="utf-8") as f: + json.dump(cmp_, f, ensure_ascii=False, indent=2, default=str) + print("comparação → telemetria_out/comparacao_treino_v8.json") + print("gráficos:", sorted(os.listdir(GRAF))) + print("não-regressão aprovada:", + cmp_.get("nao_regressao", {}).get("aprovado")) + print("ppl_lm:", cmp_["ppl_lm"]) + print("testes v6:", cmp_["test_khtst_v6"]) + print("ram:", json.dumps(cmp_["ram"])) + + +if __name__ == "__main__": + main() diff --git a/scripts/09_teste_item_b.py b/scripts/09_teste_item_b.py new file mode 100644 index 0000000000000000000000000000000000000000..b5865f1d46165d01aabf1fdbac3d00aa9689591d --- /dev/null +++ b/scripts/09_teste_item_b.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Teste FINAL do item (b) — modelo KHTST TREINADO (checkpoint fase-som). + +Verifica, com o modelo pós-treino real: + 1. 4 requisições submetidas em série ANTES da 1ª resposta; + 2. recebimento (estado RECEBIDA) confirmado para todas antes do processamento; + 3. processamento (PROCESSANDO → CONCLUIDA) com tokens gerados; + 4. stopping (cancelamento) honrado em ≤ 1 passo durante geração ativa; + 5. continue (retomada de pedido pausado). +Saída: telemetria_out/teste_item_b_v8.json +""" +import json +import sys +import threading +import time + +sys.path.insert(0, "/home/z/my-project/khtst/src") + +import torch + +from khtst.config import Config +from khtst.dados.tokenizador import TokenizadorKHTST +from khtst.memoria.orquestrador import OrquestradorSOM +from khtst.nucleo.modelo import KHTSTModel +from khtst.servico import SessaoKHTST + +CFG_PATH = "/home/z/my-project/khtst/configs/base.json" +TK_PATH = "/home/z/my-project/khtst/cache_dados/tokenizador.json" +SAIDA = "/home/z/my-project/khtst/telemetria_out/teste_item_b_v8.json" + +cfg = Config().de_arquivo(CFG_PATH) +torch.manual_seed(cfg.dados.semente) +modelo = KHTSTModel(cfg, usar_multimodal=True) +tk = TokenizadorKHTST(TK_PATH) + +# retoma o estado final (fase-som) +from khtst.dados.checkpoints import GestorCheckpoints +ck = GestorCheckpoints("/home/z/my-project/khtst/estados_local", "estados/fase-v8") +tag = ck.ultima_tag() +if tag: + ok = ck.carregar(modelo, tag) + print(f"estado retomado: {tag} (ok={ok})") +modelo.eval() + +sessao = SessaoKHTST(modelo, decodificar=tk) +prompts = [ + "O KHTST é um modelo multimodal que", + "A previsão do tempo para amanhã indica", + "Para cozinhar arroz, primeiro", + "O melhor time do Brasil é", +] +# ---- 1) 4 requisições EM SÉRIE antes da 1ª resposta ---- +pids = [sessao.submeter(p, max_novos=10, tarefa="lm") for p in prompts] +estados_iniciais = [sessao.pedidos[i].estado.value for i in pids] +check_recebimento = all(e == "recebida" for e in estados_iniciais) +print("1) 4 pedidos ANTES da 1ª resposta:", pids, "| estados:", estados_iniciais) + +# ---- 2/3) processa todos (a bomba adhoc roda ENTRE tokens) ---- +t0 = time.time() +saidas = sessao.executar_todos(max_passos=400) +dur = time.time() - t0 +rel = sessao.relatorio() +check_processamento = all(rel[i]["estado"] == "concluida" and rel[i]["n_tokens"] == 10 + for i in pids) +print("2) processamento:", [(i, rel[i]["estado"], rel[i]["n_tokens"]) for i in pids], + f"| {dur:.1f}s") +for i in pids: + texto = tk.decodificar(saidas[i]) + print(f" pedido {i}: «{prompts[i]}» → {texto[:70]!r}") + +# ---- 4) stopping durante processamento ativo (thread externa) ---- +sessao2 = SessaoKHTST(modelo, decodificar=tk) +p_st = sessao2.submeter("Escreva uma história longa sobre o mar e as estrelas", + max_novos=60, tarefa="lm") +def para_no_meio(): + time.sleep(0.4) + sessao2.stop(p_st, cancelar=True) +threading.Thread(target=para_no_meio, daemon=True).start() +sessao2.executar_todos(max_passos=400) +rel2 = sessao2.relatorio()[p_st] +check_stop = rel2["estado"] == "cancelada" and 0 < rel2["n_tokens"] < 60 +print("4) stopping:", rel2["estado"], "| tokens antes do stop:", rel2["n_tokens"], + "| eventos:", rel2["eventos"]) + +# ---- 5) pause → continue ---- +sessao3 = SessaoKHTST(modelo, decodificar=tk) +p_pc = sessao3.submeter("O livro da floresta narra", max_novos=12, tarefa="lm") +sessao3.canal.stop(p_pc, cancelar=False) # pausa no 1º dreno +def retoma(): + time.sleep(0.3) + sessao3.continuar(p_pc) +threading.Thread(target=retoma, daemon=True).start() +sessao3.executar_todos(max_passos=400) +rel3 = sessao3.relatorio()[p_pc] +check_continue = rel3["estado"] == "concluida" and "pausada" in rel3["eventos"] \ + and "retomada" in rel3["eventos"] +print("5) pause→continue:", rel3["estado"], "| eventos:", rel3["eventos"]) + +resultado = { + "modelo": "KHTST v8 (checkpoint: " + str(tag) + ")", + "item_b_4_requisicoes_antes_da_1a": bool(check_recebimento), + "item_b_processamento_e_conclusao": bool(check_processamento), + "item_b_stopping_durante_ativa": bool(check_stop), + "item_b_pause_continue": bool(check_continue), + "estados_iniciais": estados_iniciais, + "relatorio_completo": {str(i): rel[i] for i in rel}, + "relatorio_stop": rel2, + "relatorio_pause_continue": rel3, + "duracao_s": round(dur, 2), +} +ok_all = all([check_recebimento, check_processamento, check_stop, check_continue]) +resultado["item_b_aprovado"] = bool(ok_all) +with open(SAIDA, "w", encoding="utf-8") as f: + json.dump(resultado, f, ensure_ascii=False, indent=2, default=str) +print(f"\nITEM B: {'APROVADO' if ok_all else 'REPROVADO'} → {SAIDA}") +sys.exit(0 if ok_all else 1) diff --git a/src/khtst/__init__.py b/src/khtst/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/khtst/acelerado/__init__.py b/src/khtst/acelerado/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..624fe1003e1726802d7b512b887fbb8f0650143f --- /dev/null +++ b/src/khtst/acelerado/__init__.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +"""Fachada dos núcleos acelerados (Cython+C) com FALLBACK puro-torch. + +Estratégia (requisito: reescrever em Cython + C/C++ sem quebrar acessos): + • tenta importar o módulo compilado `nucleos` (build via setup.py); + • sem binário, usa os equivalentes EM TORCH com matemática idêntica + (Teorema 19.25 — semântica sequencial preservada por laço); + • `status()` informa qual backend está em uso (honestidade — doc 00). +""" +from __future__ import annotations + +import math + +import torch + +try: + from khtst.acelerado.nucleos import (som_atualizar_lote as _som_c, + contagens_entropia as _cont_c) + BACKEND = "cython_c" +except Exception: + _som_c = None + _cont_c = None + BACKEND = "torch_fallback" + + +def som_atualizar_lote(w: torch.Tensor, v: torch.Tensor, hit_ema: torch.Tensor, + z: torch.Tensor, rotulos: torch.Tensor, + eta0: float, lambda_sup: float, n_amostras: int): + """Kohonen sequencial (eq. 6.2) — Cython/C se disponível; senão torch. + Semântica EXATA do laço original do RoteadorSSOM.atualizar.""" + if _som_c is not None: + zc = z.detach().cpu().double().contiguous() + rot = rotulos.detach().cpu().long().contiguous() + # memoryviews Cython: converte via numpy; COPY-BACK garante propagação + # mesmo quando o tensor original não é float64/contíguo + wn = w.detach().cpu() + wn = wn if (wn.dtype == torch.float64 and wn.is_contiguous()) \ + else wn.double().contiguous() + vn = v.detach().cpu() + vn = vn if (vn.dtype == torch.float64 and vn.is_contiguous()) \ + else vn.double().contiguous() + hn = hit_ema.detach().cpu() + hn = hn if (hn.dtype == torch.float64 and hn.is_contiguous()) \ + else hn.double().contiguous() + eq, n = _som_c(wn.numpy(), vn.numpy(), hn.numpy(), zc.numpy(), + rot.numpy(), eta0, lambda_sup, n_amostras) + w.copy_(torch.from_numpy(wn.numpy())) + v.copy_(torch.from_numpy(vn.numpy())) + hit_ema.copy_(torch.from_numpy(hn.numpy())) + return eq, n + # fallback torch: MESMA ordem de atualizações (Robbins–Monro ordenal) + zc = z.detach().cpu().double() + eq_soma = 0.0 + for i in range(zc.shape[0]): + n_amostras += 1 + xi = zc[i] + d2 = torch.cdist(xi.unsqueeze(0), w.double()).pow(2).squeeze(0) + c = int(d2.argmin()) + eq_soma += float(d2[c]) + eta = eta0 / (1.0 + 0.001 * n_amostras) + w[c] += eta * (xi - w[c]) + p = torch.softmax(v[c], dim=0) + onehot = torch.zeros_like(p) + onehot[int(rotulos[i])] = 1.0 + v[c] += (lambda_sup * 0.1) * (onehot - p) + hit_ema *= 0.92 + hit_ema[c] += 0.08 + return eq_soma / max(zc.shape[0], 1), n_amostras + + +def contagens_entropia(contextos: list[int] | torch.Tensor, vocab: int, + h_max: float, punicao_base: float): + """Contagens + entropia + fator da punição dinâmica (eq. 10.8/10.9).""" + if _cont_c is not None and not isinstance(contextos, torch.Tensor): + ctx = torch.tensor(list(contextos), dtype=torch.long).contiguous() + cont, h, fator = _cont_c(ctx.numpy(), vocab, h_max, punicao_base) + return cont, h, fator + contagens = torch.bincount(torch.as_tensor(list(contextos), dtype=torch.long), + minlength=vocab) + probs = contagens.float() / max(float(contagens.sum()), 1.0) + p_nz = probs[probs > 0] + h = float(-(p_nz * torch.log(p_nz + 1e-8)).sum()) if p_nz.numel() else 0.0 + fator = min(1.0, h / max(h_max, 1e-6)) + return contagens, h, fator + + +def status() -> dict: + return {"backend": BACKEND, + "cython_disponivel": _som_c is not None} diff --git a/src/khtst/acelerado/setup.py b/src/khtst/acelerado/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..4bf5e5db06a2a7ca30bb6eabfc12462e4063064b --- /dev/null +++ b/src/khtst/acelerado/setup.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +"""Build dos núcleos Cython+C do KHTST: python setup.py build_ext --inplace""" +import numpy +from setuptools import Extension, setup +from Cython.Build import cythonize + +extensoes = [ + Extension( + "nucleos", + sources=["nucleos.pyx"], + include_dirs=[numpy.get_include()], + extra_compile_args=["-O3"], + language="c", + ) +] + +setup( + name="khtst-acelerado", + ext_modules=cythonize(extensoes, compiler_directives={ + "language_level": "3", + "boundscheck": False, + "wraparound": False, + "cdivision": True, + }), +) diff --git a/src/khtst/config.py b/src/khtst/config.py new file mode 100644 index 0000000000000000000000000000000000000000..6f2d3d817099855613b6f9ec5bbe647cf19fd015 --- /dev/null +++ b/src/khtst/config.py @@ -0,0 +1,307 @@ +# -*- coding: utf-8 -*- +"""Configuração central do KHTST — PT-BR total nos textos, identificadores em inglês. + +Toda constante de projeto vive aqui; scripts e módulos nunca fixam números soltos. +Carregamento: ConfigPadrao() → sobrescreve com JSON via Config.de_arquivo(caminho). +""" +from __future__ import annotations + +import json +from dataclasses import asdict, dataclass, field + + +@dataclass +class ConfigDados: + """Streaming dos datasets HF (item 4) — carregamento INDIVIDUAL (um por vez, + requisito do usuário) com orçamento de RAM (item 1). v4: +10 datasets novos.""" + datasets: list = field(default_factory=lambda: [ + # --- corpus original (8) --- + {"id": "dominguesm/restore-punctuation-ptbr-dataset", "tarefa": "pontuacao_tags", "max_exemplos": 160}, + {"id": "CEIA-POSITIVO/ultrachat_br_clustred_balanced_v1", "tarefa": "instrucao", "max_exemplos": 80}, + {"id": "dominguesm/Canarim-Instruct-PTBR-Dataset", "tarefa": "instrucao", "max_exemplos": 80}, + {"id": "adalbertojunior/punctuation-ptbr", "tarefa": "pontuacao_par", "max_exemplos": 160, + "via_parquet": True}, + {"id": "iara-project/news-articles-ptbr-dataset", "tarefa": "noticia", "max_exemplos": 140}, + {"id": "manoela/noticias_ptbr", "tarefa": "noticia", "max_exemplos": 140}, + {"id": "carolina-c4ai/corpus-carolina", "tarefa": "lm", "max_exemplos": 140}, + {"id": "BrunoN-Dev/corpus-ptbr-v1", "tarefa": "lm", "max_exemplos": 140}, + # --- v4: datasets NOVOS (requisito do usuário, streaming individual) --- + {"id": "marcelohaps/verbovision-oracle-facts-pt", "tarefa": "vqa", "max_exemplos": 80}, + {"id": "amalia-llm/MMMU-PT", "tarefa": "vqa", "max_exemplos": 80}, + {"id": "amalia-llm/MMMU-Pro-PT", "tarefa": "vqa", "max_exemplos": 80}, + {"id": "amalia-llm/OCRBench-PT", "tarefa": "ocr", "max_exemplos": 80}, + {"id": "amalia-llm/COCO-Caption2017-PT", "tarefa": "imagem_caption", "max_exemplos": 80}, + {"id": "weikaih/imaginative-perception-token-pt-eval-real", "tarefa": "vqa", "max_exemplos": 80}, + {"id": "opedromartins/asr-leaderboard-datasets-ptbr", "tarefa": "asr", "max_exemplos": 80}, + {"id": "marcosremar2/pt-br-tts-synth", "tarefa": "tts", "max_exemplos": 80}, + {"id": "leeaandrob/neurogrid-tts-synth-ptbr", "tarefa": "tts", "max_exemplos": 80}, + ]) + carregar_individual: bool = True # um dataset por vez; falha → registra e segue + comprimento_max: int = 192 # tokens por janela + semente: int = 2026 + buffer_max_ram_mb: float = 256.0 # cache em RAM limitado + cache_disco_dir: str = "/home/z/my-project/khtst/cache_dados" + + +@dataclass +class ConfigModelo: + """Modelo unificado multimodal. v4: vocab 16384, n_experts_fusao=4 + (requisitos explícitos), NLP/NLG como unidades de primeira classe. + v8 (KHTST): atencao ARVORE bidirecional fora de ordem, raio_janela e + kv_cache_max x4, MoE encoder-decoder (2 enc + 4 dec) fora de ordem, + micro buffers recorrentes e pesos de árvore bidirecionais.""" + vocab: int = 16384 # v4 — requisito do usuário + d_modelo: int = 192 + n_camadas: int = 3 + n_cabecas: int = 4 + d_ff: int = 512 + comprimento_ctx: int = 192 + janela_sliding: int = 128 # v8 — ×4 (era 32; Teorema 19.2) + n_experts_fusao: int = 4 # v4 — requisito do usuário (era 2) + dropout: float = 0.0 + kv_cache_max: int = 2048 # v8 — ×4 (era 512; RAM: ≤9,4 MB/camada) + usar_atencao_arvore: bool = True # v8 — item (f): árvore bidirecional + moe_enc_dec: dict = field(default_factory=lambda: { + # v8 — item (g): MoE encoder-decoder FORA DE ORDEM agrupável/desagrupável + "ativo": True, "n_encoders": 2, "n_decoders": 4, "d_ff_expert": 160, + "top_k": 2, "fora_de_ordem": True}) + autoajuste: dict = field(default_factory=lambda: { + # v8 — item (j): parâmetros AUTO-AJUSTÁVEIS matematicamente + "ativo": True, "janela_lr": 200, "lr_passo": 0.5, + "raio_min": 32, "raio_max": 256, "alpha_peso_arvore": 0.05, + "ppl_alvo": 17.0}) + # v2 — MoE agrupável com foco na tarefa (doc 10 §1) + moe: dict = field(default_factory=lambda: { + "ativo": True, "n_camadas_moe": 2, "n_grupos": 3, "experts_por_grupo": 2, + "d_ff_expert": 160, "top_k": 2, "margem_foco": 1.0, "n_tarefas": 9, + "vetorial": True}) # v8 — item (e): experts vetorizados (vmap/bmm) + # v2 — predição multi-token com α aprendíveis (doc 10 §3) + mtp: dict = field(default_factory=lambda: { + "ativo": True, "k_cabecas": 2, "beta_entropia": 0.01, + "k_adaptativo": True}) + # v3 — microunidades escaláveis (doc 11 §§0–5): serial/paralela/isolada/recursiva + microunidades: dict = field(default_factory=lambda: { + "d_gru": 96, "d_ramo": 96, "d_refino": 128, "kernel1": 3, "kernel2": 5, + "gamma_rec": 0.5, "k_rec_max": 2, "eps_stop": 1e-2, + "tau0": 0.8, "kappa_conf": 1.0, "delta_explora": 0.15}) + # v4 — AUTO-ESCALA por computo (doc 12): capacidade segue o ambiente + escalacao: dict = field(default_factory=lambda: { + "ativo": True, "d0": 64, "n_max_ramos": 6, "k_rec_max": 3, + "alpha": 0.5, "reavaliar_a_cada": 100}) # passos entre reavaliações + # v6 — ROTEADOR S-SOM (doc 18 §1): roteamento de MoE supervisionado + roteador: dict = field(default_factory=lambda: { + "ativo": True, "k": 24, "lambda_rota": 0.35, "gamma_empate": 0.15, + "conf_min": 0.25, "min_amostras": 200}) + # v4 — unidades NLP (processar) e NLG (gerar) de primeira classe (doc 15 §1) + nlp: dict = field(default_factory=lambda: {"ativo": True, "n_intencoes": 10}) + # v5 — atenção ENTRE camadas MoE (doc 16 §§1–2): nasce neutra (α=β=0) + atencao_moe: dict = field(default_factory=lambda: {"ativo": True, "alpha0": 0.0}) + nlg: dict = field(default_factory=lambda: {"ativo": True, "n_estilos": 8, + "peso_rerank": 0.35}) + # encoders multimodais "completo pesado" com escala configurável: + imagem: dict = field(default_factory=lambda: { + "tam_patch": 16, "resolucao": 96, "d": 192, "n_camadas": 3, "n_cabecas": 4}) + audio: dict = field(default_factory=lambda: { + "n_mels": 80, "d": 192, "n_camadas": 2, "n_cabecas": 4}) + video: dict = field(default_factory=lambda: { + "n_frames": 4, "tam_patch": 16, "resolucao": 96, "d": 128, "n_camadas": 2}) + + +@dataclass +class ConfigSOM: + """Mapas auto-organizáveis (item 2) — todas as variantes + orquestrador.""" + dim_entrada: int = 192 # = d_modelo (espaço latente compartilhado) + gg: dict = field(default_factory=lambda: {"lado": 8, "lambda_insercao": 400, "beta": 0.9995, "kmax": 64}) + gcs: dict = field(default_factory=lambda: {"kmax": 48, "lambda_insercao": 300, "beta": 0.9995}) + gsom: dict = field(default_factory=lambda: {"sf": 0.35, "lambda_vizinhos": 0.3, "max_unidades": 96}) + hsom: dict = field(default_factory=lambda: {"k_nivel1": 12, "k_filho": 10}) + tkm: dict = field(default_factory=lambda: {"k": 32, "lambda_janela": 0.5, "janela": 8}) + rsom: dict = field(default_factory=lambda: {"k": 32, "alpha": 0.3}) + cpn: dict = field(default_factory=lambda: {"k": 24, "d_saida": 64, "eta0": 0.05}) + ssom: dict = field(default_factory=lambda: {"k": 24, "lambda_sup": 0.5, "n_classes": 8}) + orquestrador: dict = field(default_factory=lambda: {"atualizacao_por_epoca": 1}) + + +@dataclass +class ConfigTreino: + """Treino extenso multi-época (item 6) com punição/retreino e W8A8 (item 9). + + v2 (doc 10): fases DENSA (máx conexões) → FOCO (top-k) → DPO → SOM (máx + neurônios ativos); PCGrad entre CE e auxiliares; barreira ABMO no lr. + """ + epocas: int = 4 + lote: int = 8 + lr_max: float = 3e-3 + lr_min: float = 3e-4 + warmup: int = 60 + clip_grad: float = 1.0 + janela_estagnacao: int = 120 # passos por janela (eq. 9.3) + epsilon_estagnacao: float = 0.01 + max_punicoes_por_epoca: int = 3 + checkpoint_a_cada: int = 150 # passos + avaliar_a_cada: int = 100 + qat_ultimos_passos: int = 80 # ativa STE-W8A8 no fim (item 9) + mistura_tarefas: dict = field(default_factory=lambda: { + "instrucao": 0.22, "lm": 0.14, "noticia": 0.1, "pontuacao": 0.1, + "imagem_caption": 0.1, "vqa": 0.1, "ocr": 0.07, "asr": 0.09, "tts": 0.08}) + # v2 — pipeline denso→SOM (item d do escopo) + fase_densa_ate_epoca: int = 3 # épocas 0..2 densas; última = foco+QAT + pcgrad: bool = True # cirurgia de gradiente CE × auxiliares + lambda_moe_lb: float = 0.01 # peso da perda de balanceamento + lambda_moe_ort: float = 0.01 # peso da penalidade ortogonal intra-grupo + lambda_mtp: float = 0.3 # peso da perda multi-token + lambda_som: float = 0.05 # peso do alinhamento SOM + # v2 — barreira analítica de LR (ABMO corrigido, doc 10 §6) + abmo: dict = field(default_factory=lambda: { + "ativo": True, "margem": 0.25, "cap_min": 0.05, "cap_max": 500.0}) + # v2 — DPO pós-SFT (doc 10 §4) + dpo: dict = field(default_factory=lambda: { + "ativo": True, "passos": 150, "beta": 0.1, "lr": 5e-5, "lr_beta": 1e-3, + "kl_alvo": 0.2, "lote": 4}) + # v2 — fase SOM com máximo de neurônios ativos (doc 10 §2.3) + fase_som: dict = field(default_factory=lambda: { + "ativo": True, "epocas": 2, "alvo_ativos": 0.90, "gamma_empate": 0.15, + "lambda_novidade": 0.25, "dim_lote": 64}) + # v3 — PRS V8 (doc 11 §7): histerese, SGDR+piso↓, clip-percentil, 2 regimes + prs_v8: dict = field(default_factory=lambda: { + "ativo": True, "eta_up": 0.05, "eta_down": 0.02, "T_ciclo": 400, + "decay_piso": 0.5, "percentil": 0.75, "janela_clip": 50, + "coef_streak": 0.3, "vel_alvo": 0.01, "boost_max": 1.5}) + # v3 — AgenteConfiança (doc 11 §8): Beta+Bernstein+PAC-Bayes, κ assimétrico + confianca: dict = field(default_factory=lambda: { + "ativo": True, "kappa_punicao": 0.3, "kappa_premio": 0.1, + "janela_tau": 50, "quantil_tau": 0.25, "h_escrita_memoria": 0.5}) + # v3 — GestorCrescimento (doc 11 §5): Hutchinson diag-H → expandir/podar + crescimento: dict = field(default_factory=lambda: { + "ativo": True, "a_cada": 200, "tau_expand": 0.10, "tau_prune": 0.01, + "eps_gate": 0.05, "janela_uso": 8, "n_max_ramos": 5, "n_sondas": 2}) + # v6 — RPP (doc 18 §2): Reward/Punishment/Penalty (Teoremas 18.4–18.7) + rpp: dict = field(default_factory=lambda: { + "ativo": True, "rho_max": 0.15, "rho_step": 0.05, "rho_decaimento": 0.5, + "janela": 30, "kappa_rotas_mortas": 0.01, "alvo_ativos": 0.90}) + # v6 — SGDR com T_mult (Teorema 18.5): ciclos T_i = T_0·T_mult^i + sgdr_t_mult: int = 2 + + +@dataclass +class ConfigMemoriaInterna: + """Memória interna multimodal (v3, doc 11 §9): slots conficientes, evicção + por utilidade, compressão VQ e índice SOM + contratos de acesso.""" + ativo: bool = True + n_slots: int = 64 + h_escrita: float = 0.5 + idade_compressao_s: float = 900.0 + n_codigos: int = 64 + lambda_utilidade: float = 0.1 + meia_vida_s: float = 1800.0 + replay_a_cada: int = 40 # passos entre replays de memória como contexto + + +@dataclass +class ConfigJanela1M: + """Janela de contexto com compressão INDEXADA (v4, doc 13). + v8 (KHTST): 256K tokens (requisito) — m_max recalculado: + m_max = ⌈(n_max − l_fino)/s_passo⌉ + 1 = ⌈(262144−4096)/128⌉ + 1 = 2017.""" + ativo: bool = True + n_max_tokens: int = 262_144 # v8 — 256K tokens (requisito) + l_fino: int = 4096 # janela fina (atenção normal) + w_segmento: int = 256 # tamanho do segmento/resumo + s_passo: int = 128 # passo com overlap 50% + k_recall: int = 4 # resumos recuperados por consulta + viz_r: int = 2 # anel da busca coarse (Teorema 13.1) + m_max: int = 2017 # v8 — orçamento de resumos p/ 256K (eq. 13.1) + + +@dataclass +class ConfigCheckpoints: + """Checkpoints LOCAIS do treino (item c — doc 16 §8). + + v5: nenhum upload parcial no Hub durante o treino (o Hub bloqueia + envios parciais em sequência curta). O snapshot final entra no commit + único de publicação (GestorCheckpoints.preparar_para_publicacao).""" + dir_local: str = "/home/z/my-project/khtst/estados_local" + pasta_publicacao: str = "estados/fase-v6" + + +@dataclass +class ConfigSOMAtencao: + """Atenção ENTRE variantes Kohonen (v5, doc 16 §§3–5): recall atencional + sobre TODAS as variantes + invariante ZERO neurônios isolados.""" + ativo: bool = True + tau: float = 0.5 # temperatura do softmax de variantes + gamma_novidade: float = 0.15 # bônus a variantes com órfãos (Teo. 16.5) + + +@dataclass +class ConfigDifusao: + """Difusão multimodal (v5, doc 17): SD txt2img/img2img/inpaint com ciclo + fechado de compreensão geracional. Default: repositório TINY (testes); + produção: stabilityai/stable-diffusion-2-1-base (GPU ≥ 8GB).""" + ativo: bool = True + repo_id: str = "hf-internal-testing/tiny-stable-diffusion-torch" + altura: int = 128 + largura: int = 128 + passos_inferencia: int = 8 + guia_escala: float = 7.5 + + +@dataclass +class ConfigAgenteEngenheiro: + """Agente Engenheiro (v5, doc 16 §9): revisão pré-modificação + + observação de TODAS as métricas fundamentais por módulo (treino+inferência).""" + ativo: bool = True + intervalo: int = 50 # passos entre observações completas + tolerancia_regressao: float = 0.05 + + +@dataclass +class ConfigTelemetria: + """Telemetria/indicadores (item 5).""" + arquivo_jsonl: str = "/home/z/my-project/khtst/telemetria_out/telemetria.jsonl" + ema_beta: float = 0.98 + psi_limite: float = 0.25 + rank_efectivo_min: float = 4.0 + grad_faixa: tuple = field(default_factory=lambda: (1e-7, 1e2)) + + +@dataclass +class ConfigCiclo: + """Raciocínio cíclico PDCA (item 3) + ferramentas com cache (item 10).""" + tau_escalada: float = 0.35 # p abaixo disso → humano (Teorema 9.5) + entropia_limite: float = 0.85 + orcamento_passos: int = 6 + cache_ferramentas_ttl: int = 600 # s + cache_ferramentas_max: int = 64 + + +@dataclass +class Config: + dados: ConfigDados = field(default_factory=ConfigDados) + modelo: ConfigModelo = field(default_factory=ConfigModelo) + som: ConfigSOM = field(default_factory=ConfigSOM) + treino: ConfigTreino = field(default_factory=ConfigTreino) + telemetria: ConfigTelemetria = field(default_factory=ConfigTelemetria) + ciclo: ConfigCiclo = field(default_factory=ConfigCiclo) + checkpoints: ConfigCheckpoints = field(default_factory=ConfigCheckpoints) + memoria_interna: ConfigMemoriaInterna = field(default_factory=ConfigMemoriaInterna) + janela_1m: ConfigJanela1M = field(default_factory=ConfigJanela1M) + som_atencao: ConfigSOMAtencao = field(default_factory=ConfigSOMAtencao) + difusao: ConfigDifusao = field(default_factory=ConfigDifusao) + agente_engenheiro: ConfigAgenteEngenheiro = field( + default_factory=ConfigAgenteEngenheiro) + + def de_arquivo(self, caminho: str) -> "Config": + """Sobrescreve campos recursivamente a partir de JSON.""" + with open(caminho, "r", encoding="utf-8") as f: + sobrescrita = json.load(f) + for nome_secao, valores in sobrescrita.items(): + secao = getattr(self, nome_secao, None) + if secao is None: + setattr(self, nome_secao, valores) + continue + for k, v in valores.items(): + setattr(secao, k, v) + return self + + def para_dict(self) -> dict: + return asdict(self) diff --git a/src/khtst/dados/__init__.py b/src/khtst/dados/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/khtst/dados/cache.py b/src/khtst/dados/cache.py new file mode 100644 index 0000000000000000000000000000000000000000..254f310474c03f62b0ed2342c4989b647bccda44 --- /dev/null +++ b/src/khtst/dados/cache.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +"""Cache em RAM e disco com orçamento explícito (item 1: otimização de RAM +e armazenamento). RAM: LRU por bytes estimados; Disco: arquivos .pt nomeados +por hash, com índice JSON e limites de total. + +Política: prioridade RAM para tensores quentes (lote corrente), disco para +blobs frios (embeddings pré-calculados, amostras do corpus). +""" +from __future__ import annotations + +import hashlib +import json +import os +from collections import OrderedDict + +import torch + + +def _estimar_bytes(obj) -> int: + if isinstance(obj, torch.Tensor): + return obj.element_size() * obj.numel() + if isinstance(obj, (list, tuple)): + return sum(_estimar_bytes(o) for o in obj) + 64 + if isinstance(obj, dict): + return sum(_estimar_bytes(k) + _estimar_bytes(v) for k, v in obj.items()) + 64 + return len(str(obj).encode("utf-8")) + 48 + + +def _chave_hash(chave: str) -> str: + return hashlib.sha1(chave.encode("utf-8")).hexdigest()[:16] + + +class CacheRAM: + """LRU com limite de bytes. Evict: menos-recentemente-usado primeiro.""" + + def __init__(self, limite_mb: float = 256.0): + self.limite = int(limite_mb * 1024 * 1024) + self._uso = 0 + self._mapa: OrderedDict[str, tuple] = OrderedDict() # chave -> (valor, bytes) + + def get(self, chave: str): + item = self._mapa.get(chave) + if item is None: + return None + self._mapa.move_to_end(chave) # toca LRU + return item[0] + + def put(self, chave: str, valor) -> None: + if chave in self._mapa: + self._uso -= self._mapa[chave][1] + del self._mapa[chave] + n_bytes = _estimar_bytes(valor) + if n_bytes > self.limite: # indivisível e maior que tudo: descarta + return + while self._uso + n_bytes > self.limite and self._mapa: + _, (_, b) = self._mapa.popitem(last=False) + self._uso -= b + self._mapa[chave] = (valor, n_bytes) + self._uso += n_bytes + + @property + def uso_mb(self) -> float: + return self._uso / 1024 / 1024 + + def __len__(self): + return len(self._mapa) + + +class CacheDisco: + """Blobs frios em disco com índice JSON e limite de armazenamento.""" + + def __init__(self, diretorio: str, limite_mb: float = 2048.0): + self.dir = diretorio + os.makedirs(diretorio, exist_ok=True) + self.indice_path = os.path.join(diretorio, "indice.json") + self.limite = limite_mb * 1024 * 1024 + self.indice: dict[str, dict] = self._carregar_indice() + + def _carregar_indice(self) -> dict: + if os.path.exists(self.indice_path): + with open(self.indice_path, "r", encoding="utf-8") as f: + return json.load(f) + return {} + + def _salvar_indice(self) -> None: + with open(self.indice_path, "w", encoding="utf-8") as f: + json.dump(self.indice, f, ensure_ascii=False) + + def _evict_se_necessario(self, novo_bytes: int) -> None: + total = sum(m["bytes"] for m in self.indice.values()) + while total + novo_bytes > self.limite and self.indice: + chave_mais_velha = min(self.indice, key=lambda k: self.indice[k]["t"]) + meta = self.indice.pop(chave_mais_velha) + try: + os.remove(meta["arquivo"]) + except FileNotFoundError: + pass + total -= meta["bytes"] + + def put(self, chave: str, tensor: torch.Tensor) -> None: + h = _chave_hash(chave) + caminho = os.path.join(self.dir, h + ".pt") + torch.save(tensor.cpu(), caminho) + n_bytes = os.path.getsize(caminho) + self._evict_se_necessario(n_bytes) + self.indice[chave] = {"arquivo": caminho, "bytes": n_bytes, "t": os.path.getmtime(caminho)} + self._salvar_indice() + + def get(self, chave: str) -> torch.Tensor | None: + meta = self.indice.get(chave) + if meta is None or not os.path.exists(meta["arquivo"]): + return None + return torch.load(meta["arquivo"], map_location="cpu", weights_only=True) + + +class CacheDuplo: + """Fachada: RAM primeiro, disco como respaldo. Métricas p/ telemetria.""" + + def __init__(self, limite_ram_mb: float, dir_disco: str, limite_disco_mb: float = 2048.0): + self.ram = CacheRAM(limite_ram_mb) + self.disco = CacheDisco(dir_disco, limite_disco_mb) + self.estatisticas = {"hits_ram": 0, "hits_disco": 0, "misses": 0} + + def get(self, chave: str): + v = self.ram.get(chave) + if v is not None: + self.estatisticas["hits_ram"] += 1 + return v + v = self.disco.get(chave) + if v is not None: + self.estatisticas["hits_disco"] += 1 + self.ram.put(chave, v) # promove + return v + self.estatisticas["misses"] += 1 + return None + + def put(self, chave: str, valor, persistir: bool = False) -> None: + self.ram.put(chave, valor) + if persistir and isinstance(valor, torch.Tensor): + self.disco.put(chave, valor) diff --git a/src/khtst/dados/checkpoints.py b/src/khtst/dados/checkpoints.py new file mode 100644 index 0000000000000000000000000000000000000000..de9c9d11bd065c4a29c880e80391e4ff22e164d6 --- /dev/null +++ b/src/khtst/dados/checkpoints.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +"""Checkpoints de treino — v5 (doc 16 §8). + +Substitui `dados/estados_hf.py` (REMOVIDO a pedido — estados/fase-som-v4 +apagados do Hub e do disco). Regra v5 motivada pelo bloqueio do Hub a +uploads parciais em sequência curta: + +• durante o treino, checkpoints ficam SEMPRE LOCAIS (safetensors + SHA-256, + protocolo idêntico ao doc 10 §9) e o diretório mantém só o último + (poupa disco, item c do escopo); +• NENHUM upload parcial durante o treino; +• o snapshot FINAL entra no commit ÚNICO da publicação (scripts/ + 06_publicar_hf.py chama `preparar_para_publicacao`, que copia o par + (modelo.safetensors, meta.json) para o stage) — um único commit organizado. + +Teorema 16.9 (integridade do snapshot): seja σ o par gravado atomicamente +com hash h(σ)=SHA-256. Na leitura, se h(σ')=h(σ) então σ'=σ (colisão +desprezível, 2^−256), logo retomadas reproduzem EXATAMENTE o estado salvo. +""" +from __future__ import annotations + +import hashlib +import json +import os +import shutil + +import torch + +from safetensors.torch import load as _st_load +from safetensors.torch import save as _st_save + + +def _sha256_bytes(b: bytes) -> str: + return hashlib.sha256(b).hexdigest() + + +class GestorCheckpoints: + """Checkpoints locais atômicos + snapshot final para publicação única. + + Args: + dir_local: diretório de checkpoints (mantém apenas o último). + pasta_publicacao: pasta dentro do repo HF onde o snapshot final + entra NO commit único (ex.: 'estados/fase-v5'). + """ + + def __init__(self, dir_local: str = "/home/z/my-project/khtst/estados_local", + pasta_publicacao: str = "estados/fase-v5"): + self.dir_local = dir_local + self.pasta_publicacao = pasta_publicacao + self.eventos: list[str] = [] + os.makedirs(dir_local, exist_ok=True) + + # ---------------- protocolo (doc 10 §9 preservado) ---------------- + def _caminho_tag(self, tag: str) -> str: + return os.path.join(self.dir_local, tag) + + def salvar(self, modelo, tag: str, meta: dict) -> str: + """Serializa safetensors + meta com hash; mantém apenas a última tag.""" + sd = {k: v.detach().contiguous() for k, v in modelo.state_dict().items()} + # pesos empatados (tied): uma única cópia (load com strict=False) + vistos: dict[int, str] = {} + sd_unico: dict[str, torch.Tensor] = {} + for k, v in sd.items(): + ptr = v.data_ptr() + if ptr in vistos: + continue + vistos[ptr] = k + sd_unico[k] = v + dados = bytes(_st_save(sd_unico, metadata={"formato": "khtst-v5", "tag": tag})) + meta = dict(meta or {}) + meta["hash_sha256"] = _sha256_bytes(dados) + meta["tag"] = tag + destino = self._caminho_tag(tag) + os.makedirs(destino, exist_ok=True) + with open(os.path.join(destino, "modelo.safetensors"), "wb") as f: + f.write(dados) + with open(os.path.join(destino, "meta.json"), "w", encoding="utf-8") as f: + json.dump(meta, f, ensure_ascii=False, indent=2) + # higiene: mantém SOMENTE a última tag (poupa disco, item c) + removidos = 0 + for nome in os.listdir(self.dir_local): + if nome != tag: + shutil.rmtree(os.path.join(self.dir_local, nome), + ignore_errors=True) + removidos += 1 + if removidos: + self.eventos.append(f"{removidos} checkpoint(s) antigo(s) apagado(s)") + self.eventos.append(f"checkpoint {tag}: local ({len(dados)/1e6:.1f} MB)") + return os.path.join(destino, "modelo.safetensors") + + def carregar(self, modelo, tag: str | None = None) -> bool: + """Carrega tolerante a forma (mesma política da v3: tensores por + chave com mesma forma; divergências registradas).""" + tag = tag or self.ultima_tag() + if tag is None: + return False + caminho = os.path.join(self._caminho_tag(tag), "modelo.safetensors") + if not os.path.exists(caminho): + self.eventos.append(f"checkpoint {tag}: ausente") + return False + with open(caminho, "rb") as f: + dados = f.read() + meta_path = os.path.join(self._caminho_tag(tag), "meta.json") + if os.path.exists(meta_path): + with open(meta_path, encoding="utf-8") as f: + esperado = json.load(f).get("hash_sha256") + if esperado and _sha256_bytes(dados) != esperado: + self.eventos.append(f"checkpoint {tag}: HASH divergente — rejeitado") + return False + try: + sd = _st_load(dados) + modelo_sd = modelo.state_dict() + próprios = {k: v for k, v in sd.items() + if k in modelo_sd and modelo_sd[k].shape == v.shape} + pulados = len(sd) - len(próprios) + faltantes = [k for k in modelo_sd if k not in próprios and k not in sd] + modelo.load_state_dict(próprios, strict=False) + self.eventos.append( + f"checkpoint {tag}: {len(próprios)} tensores, {pulados} pulados, " + f"{len(faltantes)} novos") + return True + except Exception as e: + self.eventos.append(f"carregar {tag}: {type(e).__name__}: {e}") + return False + + def listar(self) -> list: + """Tags disponíveis localmente, ordenadas lexicograficamente + (tags canônicas `epoca-*`/`fase-*` são cronológicas).""" + if not os.path.isdir(self.dir_local): + return [] + return sorted(d for d in os.listdir(self.dir_local) + if os.path.isdir(os.path.join(self.dir_local, d)) + and os.path.exists(os.path.join(self.dir_local, d, + "meta.json"))) + + def ultima_tag(self) -> str | None: + if not os.path.isdir(self.dir_local): + return None + tags = [d for d in os.listdir(self.dir_local) + if os.path.isdir(os.path.join(self.dir_local, d)) + and os.path.exists(os.path.join(self.dir_local, d, "meta.json"))] + if not tags: + return None + # mais avançado = maior passo_global no meta (ordem cronológica) + melhor, melhor_passo = None, -1 + for tag in tags: + try: + with open(os.path.join(self.dir_local, tag, "meta.json"), + encoding="utf-8") as f: + passo = int(json.load(f).get("passo_global", -1)) + except Exception: + continue + if passo > melhor_passo: + melhor, melhor_passo = tag, passo + return melhor + + def meta_de(self, tag: str | None = None) -> dict: + tag = tag or self.ultima_tag() + if tag is None: + return {} + caminho = os.path.join(self._caminho_tag(tag), "meta.json") + if not os.path.exists(caminho): + return {} + with open(caminho, encoding="utf-8") as f: + return json.load(f) + + # ---------------- publicação ÚNICA (doc 16 §8) ---------------- + def preparar_para_publicacao(self, stage_raiz: str) -> str | None: + """Copia o snapshot final para o stage do repo (commit único do + script 06). Devolve o caminho do peso no stage.""" + tag = self.ultima_tag() + if tag is None: + self.eventos.append("publicação: nenhum checkpoint local") + return None + origem = self._caminho_tag(tag) + destino = os.path.join(stage_raiz, self.pasta_publicacao) + os.makedirs(destino, exist_ok=True) + shutil.copy2(os.path.join(origem, "modelo.safetensors"), + os.path.join(destino, "modelo.safetensors")) + meta = self.meta_de(tag) + meta["publicado_em_commit_unico"] = True + with open(os.path.join(destino, "meta.json"), "w", encoding="utf-8") as f: + json.dump(meta, f, ensure_ascii=False, indent=2) + self.eventos.append(f"snapshot {tag} → stage ({self.pasta_publicacao})") + return os.path.join(destino, "modelo.safetensors") diff --git a/src/khtst/dados/documentos.py b/src/khtst/dados/documentos.py new file mode 100644 index 0000000000000000000000000000000000000000..f445619d7abe4d8b4fa8e60a95b63b572f1d5e09 --- /dev/null +++ b/src/khtst/dados/documentos.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +"""Parsers de documentos (item 1: planilha, documentos, PDF) com degradação +graciosa: dependências opcionais (pypdf, openpyxl) e saída de texto limpa. + +Todas as funções retornam str (vazio quando não suportado) e nunca lançam +exceção para entrada malformada — violações de contrato são reportadas pelo +chamador quando o texto vazio viola expectativa. +""" +from __future__ import annotations + +import csv +import io +import os + + +def texto_de_pdf(caminho: str) -> str: + try: + from pypdf import PdfReader + except ImportError: + return "" + try: + leitor = PdfReader(caminho) + return "\n".join((p.extract_text() or "") for p in leitor.pages).strip() + except Exception: + return "" + + +def texto_de_planilha(caminho: str) -> str: + """CSV/XLSX → texto linear por linhas (células separadas por ' | ').""" + ext = os.path.splitext(caminho)[1].lower() + try: + if ext == ".csv": + with open(caminho, "r", encoding="utf-8", errors="replace", newline="") as f: + return "\n".join(" | ".join(linha) for linha in csv.reader(f)).strip() + if ext == ".xlsx": + try: + from openpyxl import load_workbook + except ImportError: + return "" + wb = load_workbook(caminho, read_only=True, data_only=True) + linhas = [] + for ws in wb.worksheets: + for row in ws.iter_rows(values_only=True): + if row: + linhas.append(" | ".join("" if c is None else str(c) for c in row)) + return "\n".join(linhas).strip() + except Exception: + return "" + return "" + + +def texto_de_docx(caminho: str) -> str: + try: + from docx import Document # python-docx + doc = Document(caminho) + return "\n".join(p.text for p in doc.paragraphs if p.text.strip()).strip() + except ImportError: + return "" + except Exception: + return "" + + +def texto_de_arquivo(caminho: str) -> str: + ext = os.path.splitext(caminho)[1].lower() + if ext == ".pdf": + return texto_de_pdf(caminho) + if ext in (".csv", ".xlsx", ".xls", ".tsv"): + return texto_de_planilha(caminho) + if ext == ".docx": + return texto_de_docx(caminho) + try: + with open(caminho, "r", encoding="utf-8", errors="replace") as f: + return f.read().strip() + except Exception: + return "" diff --git a/src/khtst/dados/streaming.py b/src/khtst/dados/streaming.py new file mode 100644 index 0000000000000000000000000000000000000000..6751abb7e64345dc96270c1dbe2637a89cc3d984 --- /dev/null +++ b/src/khtst/dados/streaming.py @@ -0,0 +1,576 @@ +# -*- coding: utf-8 -*- +"""Streaming dos datasets HF v2 (item 4b) → registros unificados multimodais. + +Regras do escopo v2: +• streaming=True em tudo (e trust_remote_code=True onde aceito — datasets 5.x + emite aviso e ignora quando obsoleto); parquet dirigido só onde não há stream; +• UM DATASET POR VEZ (sequencial), com evento de telemetria e limpeza de cache + temporário após cada fonte (orçamento de armazenamento); +• multimodal REAL: imagens reduzidas p/ 96px JPEG; áudio = waveform fp16 16kHz + limitado a 2s (o encoder Whisper-like calcula o log-mel internamente); +• webdataset (tar) acessado por AMOSTRAGEM DE FAIXA HTTP (Range) — não baixa + shards de centenas de MB para pegar dezenas de membros. + +Registro unificado: + {"fonte": str, "tarefa": str, "texto": str|None, "entrada": str|None, + "saida": str|None, "imagem": bytes|None (JPEG), "audio": bytes|None + (float16 little-endian 16kHz), "meta": dict} + +Tarefas: lm, noticia, instrucao, pontuacao, imagem_caption, vqa, ocr, asr, tts. +""" +from __future__ import annotations + +import ast +import base64 +import hashlib +import io +import json +import os +import shutil + +import numpy as np +import pyarrow.parquet as pq +from datasets import load_dataset + +TAREFAS = ("lm", "noticia", "instrucao", "pontuacao", "imagem_caption", + "vqa", "ocr", "asr", "tts") +SR_AUDIO = 16000 +MAX_SEG_AUDIO = 2.0 # s — orçamento de RAM/armazenamento +LADO_IMAGEM = 96 # px +QUALIDADE_JPEG = 70 + +CACHE_TMP = "/home/z/my-project/cache_hub_tmp" + + +def _hash_texto(t: str) -> str: + return hashlib.sha1(t.encode("utf-8", "ignore")).hexdigest()[:16] + + +def limpar_cache_temp(): + """Apaga o cache temporário de downloads (um dataset por vez → disco sob controle).""" + if os.path.isdir(CACHE_TMP): + shutil.rmtree(CACHE_TMP, ignore_errors=True) + os.makedirs(CACHE_TMP, exist_ok=True) + + +class EventoDataset: + def __init__(self, fonte: str, status: str, detalhe: str = "", n: int = 0): + self.fonte, self.status, self.detalhe, self.n = fonte, status, detalhe, n + + def __repr__(self): + return f"[{self.status}] {self.fonte}: {self.detalhe} (n={self.n})" + + +# -------------------------------------------------------------------------- +# utilidades multimodais +# -------------------------------------------------------------------------- +def _reduzir_imagem(dados: bytes | None, pil_img=None) -> bytes | None: + """Decodifica (bytes ou PIL), converte RGB, reduz p/ LADO_IMAGEM e codifica JPEG.""" + try: + from PIL import Image + if pil_img is None: + if not dados: + return None + img = Image.open(io.BytesIO(dados)) + else: + img = pil_img + img = img.convert("RGB") + img.thumbnail((LADO_IMAGEM, LADO_IMAGEM)) + buf = io.BytesIO() + img.save(buf, "JPEG", quality=QUALIDADE_JPEG) + return buf.getvalue() + except Exception: + return None + + +def _normalizar_audio(dados: bytes) -> tuple[bytes, float] | None: + """WAV/FLAC/MP3 (soundfile) → waveform mono fp16 @SR_AUDIO, cap MAX_SEG_AUDIO. + Devolve (bytes, duração_s) ou None.""" + try: + import soundfile as sf + y, sr = sf.read(io.BytesIO(dados), dtype="float32", always_2d=False) + if y.ndim > 1: + y = y.mean(axis=1) + if sr != SR_AUDIO: # reamostragem linear (leve) + n_alvo = int(len(y) * SR_AUDIO / sr) + y = np.interp(np.linspace(0.0, len(y) - 1, n_alvo), np.arange(len(y)), y) + cap = int(MAX_SEG_AUDIO * SR_AUDIO) + dur = min(len(y) / SR_AUDIO, MAX_SEG_AUDIO) + if len(y) < SR_AUDIO // 2: # < 0,5 s: inútil + return None + y = y[:cap] + pico = float(np.abs(y).max()) or 1.0 + y = (y / max(pico, 1e-6)).astype(np.float16) + return y.tobytes(), dur + except Exception: + return None + + +def _b64(b: bytes | None) -> str | None: + return base64.b64encode(b).decode("ascii") if b else None + + +def _de_b64(s: str | None) -> bytes | None: + return base64.b64decode(s) if s else None + + +# -------------------------------------------------------------------------- +# Adaptadores: exemplo bruto -> registro unificado (ou None para pular) +# -------------------------------------------------------------------------- +def _ad_pontuacao_dominguesm(ex): + texto = (ex.get("text") or "").strip() + tokens = ex.get("tokens") or [] + if len(texto) < 40 or len(tokens) < 8: + return None + return {"tarefa": "pontuacao", "entrada": " ".join(tokens), "saida": texto, + "texto": None} + + +def _ad_ultrachat(ex): + conversa = ex.get("conversa") or [] + usr = next((c["content"] for c in conversa if c.get("role") == "user"), None) + ast_ = next((c["content"] for c in conversa if c.get("role") == "assistant"), None) + if not usr or not ast_ or len(usr) + len(ast_) < 40: + return None + return {"tarefa": "instrucao", "entrada": usr.strip(), "saida": ast_.strip(), + "texto": None} + + +def _ad_canarim(ex): + instr = (ex.get("instruction") or "").strip() + inp = (ex.get("input") or "").strip() + out = (ex.get("output") or "").strip() + if not instr or not out: + return None + return {"tarefa": "instrucao", "entrada": instr + ("\n" + inp if inp else ""), + "saida": out, "texto": None} + + +def _ad_noticia(ex): + titulo = (ex.get("title") or "").strip() + texto = (ex.get("text") or "").strip() + if len(texto) < 80 or not titulo: + return None + return {"tarefa": "noticia", "entrada": None, "saida": None, + "texto": f"{titulo}\n{texto[:1500]}"} + + +def _ad_brunon(ex): + t = (ex.get("text") or "").strip() + if len(t) < 120: + return None + return {"tarefa": "lm", "entrada": None, "saida": None, "texto": t[:2000]} + + +def _ad_pontuacao_adalberto(ex): + tokens = ex.get("tokens") or [] + t = " ".join(tokens).strip() + if len(t) < 80: + return None + return {"tarefa": "lm", "entrada": None, "saida": None, "texto": t} + + +# ---- multimodais v2 ------------------------------------------------------- +def _primeira_imagem(ex, chaves=("image", "imagem", "image_1", "img")): + """Devolve (bytes|None, PIL|None) da primeira coluna de imagem utilizável.""" + from PIL import Image + for ch in chaves: + v = ex.get(ch) + if v is None: + continue + if isinstance(v, dict) and v.get("bytes"): + return v["bytes"], None + if isinstance(v, (bytes, bytearray)): + return bytes(v), None + if isinstance(v, Image.Image): + return None, v + return None, None + + +def _ad_verbovision(ex): + """marcelohaps/verbovision-oracle-facts-pt: imagem + caption + fatos. + tarefa imagem_caption: entrada = fatos, saída = caption (verbo-visual).""" + cap = (ex.get("caption") or "").strip() + fatos = ex.get("fatos") or [] + if len(cap) < 30 or not fatos: + return None + dados, pil = _primeira_imagem(ex, ("imagem", "image")) + return {"tarefa": "imagem_caption", "texto": None, + "entrada": "Fatos: " + "; ".join(str(f) for f in fatos[:8])[:400], + "saida": cap, "imagem": _reduzir_imagem(dados, pil)} + + +def _opcoes_para_texto(opcoes): + """opções vêm como string repr de lista → parse seguro.""" + if isinstance(opcoes, (list, tuple)): + vals = list(opcoes) + else: + try: + vals = ast.literal_eval(str(opcoes)) + if not isinstance(vals, (list, tuple)): + vals = [str(opcoes)] + except Exception: + vals = [str(opcoes)] + return "; ".join(str(v).strip() for v in vals) + + +def _ad_mmmu(ex): + """amalia-llm/MMMU-PT e MMMU-Pro-PT: questão multimodal com opções.""" + q = (ex.get("question") or "").strip() + resp = _texto_de(ex.get("answer")) + if not q or not resp: + return None + opts = _opcoes_para_texto(ex.get("options")) + expl = (ex.get("explanation") or "").strip() + saida = resp + (f" — {expl[:200]}" if expl else "") + dados, pil = _primeira_imagem(ex, tuple(f"image_{i}" for i in range(1, 8))) + return {"tarefa": "vqa", "texto": None, + "entrada": f"{q}\nOpções: {opts}"[:900], "saida": saida, + "imagem": _reduzir_imagem(dados, pil)} + + +def _ad_ocrbench(ex): + """amalia-llm/OCRBench-PT: imagem + pergunta + resposta(s).""" + q = (ex.get("question") or "").strip() + resp = ex.get("answer") + if not q or not resp: + return None + if isinstance(resp, (list, tuple)): + saida = ", ".join(str(r) for r in resp) + else: + saida = str(resp) + if not saida.strip(): + return None + dados, pil = _primeira_imagem(ex, ("image", "imagem")) + return {"tarefa": "ocr", "texto": None, "entrada": q, "saida": saida[:200], + "imagem": _reduzir_imagem(dados, pil)} + + +def _texto_de(v) -> str: + """Normaliza respostas que vêm como str OU lista.""" + if isinstance(v, (list, tuple)): + return ", ".join(str(x).strip() for x in v if str(x).strip()) + return str(v or "").strip() + + +def _ad_coco_pt(ex): + """amalia-llm/COCO-Caption2017-PT: imagem + comando + legenda PT.""" + resp = _texto_de(ex.get("answer")) + q = (ex.get("question") or "").strip() or "Descreva a imagem." + if len(resp) < 12: + return None + dados, pil = _primeira_imagem(ex, ("image", "imagem")) + return {"tarefa": "imagem_caption", "texto": None, "entrada": q, + "saida": resp, "imagem": _reduzir_imagem(dados, pil)} + + +def _ad_weikaih(ex): + """weikaih/imaginative-perception-token-pt-eval-real (config td_path): + question + choices + answer + imagem (struct).""" + q = (ex.get("question") or "").strip() + resp = (ex.get("answer") or "").strip() + if not q or not resp: + return None + escolhas = ex.get("choices") or [] + opts = "; ".join(str(c) for c in escolhas) if isinstance(escolhas, list) \ + else str(escolhas) + dados, pil = _primeira_imagem(ex, ("image", "imagem")) + return {"tarefa": "vqa", "texto": None, + "entrada": f"{q}\nOpções: {opts}"[:900], "saida": resp, + "imagem": _reduzir_imagem(dados, pil)} + + +def _coluna_transcricao(ex): + for ch in ("transcription", "clean_transcription", "sentence", "text", + "transcript", "ground_truth"): + v = ex.get(ch) + if isinstance(v, str) and len(v.strip()) >= 10: + return v.strip() + return None + + +def _ad_asr(ex): + """opedromartins/asr-leaderboard-datasets-ptbr (configs mls/tedx/fleurs/…): + áudio REAL (decode=False) + transcrição → tarefa asr.""" + t = _coluna_transcricao(ex) + a = ex.get("audio") + if not t or not isinstance(a, dict) or not a.get("bytes"): + return None + norm = _normalizar_audio(a["bytes"]) + if norm is None: + return None + onda, dur = norm + return {"tarefa": "asr", "texto": None, + "entrada": f"Transcreva o áudio ({dur:.1f}s):", + "saida": t, "imagem": None, "audio": onda, + "meta": {"dur_s": round(dur, 2)}} + + +# ---- webdataset por faixa de bytes (sem baixar shards gigantes) ----------- +def _amostrar_tar(repo_id: str, arquivo: str, faixa: int = 3_000_000, + max_wav: int = 16, token: str | None = None): + """Amostra os PRIMEIROS membros .wav de um tar via HTTP Range (206) — + custo de rede ≈ `faixa` bytes em vez do shard inteiro (vários GB).""" + import requests + url = f"https://huggingface.co/datasets/{repo_id}/resolve/main/{arquivo}" + cab = {"Range": f"bytes=0-{faixa}"} + if token: + cab["Authorization"] = f"Bearer {token}" + try: + r = requests.get(url, headers=cab, stream=True, timeout=90) + if r.status_code not in (200, 206): + return [] + import tarfile + buf = io.BytesIO(r.raw.read(faixa)) + tf = tarfile.open(fileobj=buf, mode="r|") + wavs = [] + for m in tf: + if m.isfile() and m.name.endswith(".wav"): + dados = tf.extractfile(m).read() + wavs.append((os.path.basename(m.name), dados)) + if len(wavs) >= max_wav: + break + return wavs + except Exception: + return [] + + +def _registros_tts_marcos(token: str | None, n_texto: int, n_audio: int): + """marcosremar2/pt-br-tts-synth: metadata.jsonl (transcripts completos, + 0 downloads de áudio) + amostra REAL de áudio por faixa HTTP (asr sintético).""" + from huggingface_hub import hf_hub_download + registros = [] + meta_path = hf_hub_download(repo_id="marcosremar2/pt-br-tts-synth", + repo_type="dataset", filename="metadata.jsonl", + token=token, cache_dir=CACHE_TMP) + indice = {} + with open(meta_path, encoding="utf-8") as f: + for linha in f: + try: + m = json.loads(linha) + except Exception: + continue + texto = (m.get("text") or "").strip() + if len(texto) < 6: + continue + indice[m.get("file_name", "")] = m + if len(registros) < n_texto: + tier = m.get("tier", "simples") + topico = m.get("topic", "geral") + registros.append({ + "tarefa": "tts", "texto": None, + "entrada": f"Gere a fala ({tier}, tópico: {topico}):", + "saida": texto, "imagem": None, "audio": None, + "meta": {"voz": m.get("voice"), "nivel": tier}}) + # áudio real sintetizado por faixa HTTP (até n_audio membros com transcript) + wavs = _amostrar_tar("marcosremar2/pt-br-tts-synth", "shard_000.tar", + max_wav=n_audio * 2, token=token) + pegos = 0 + for nome, dados in wavs: + if pegos >= n_audio: + break + m = indice.get(nome) + if not m: + continue + norm = _normalizar_audio(dados) + if norm is None: + continue + onda, dur = norm + registros.append({"tarefa": "asr", "texto": None, + "entrada": f"Transcreva o áudio ({dur:.1f}s):", + "saida": m["text"].strip(), "imagem": None, + "audio": onda, "meta": {"dur_s": round(dur, 2), + "origem": "tts_synth"}}) + pegos += 1 + return registros + + +def _registros_tts_leeaandrob(token: str | None, n_audio: int): + """leeaandrob/neurogrid-tts-synth-ptbr: tars de 4,9/9GB — AMOSTRAGEM por + faixa HTTP; pares (wav, texto) se houver .txt/.json com mesmo stem.""" + wavs = _amostrar_tar("leeaandrob/neurogrid-tts-synth-ptbr", + "synth_clean_v1.tar", max_wav=n_audio * 4, token=token) + por_stem = {} + for nome, dados in wavs: + stem, ext = os.path.splitext(os.path.basename(nome)) + por_stem.setdefault(stem, {})[ext] = dados + registros = [] + for stem, partes in por_stem.items(): + if ".wav" not in partes: + continue + texto = None + for ext in (".txt", ".json", ".lab"): + if ext in partes: + bruto = partes[ext].decode("utf-8", "ignore") + if ext == ".json": + try: + j = json.loads(bruto) + texto = j.get("text") or j.get("transcription") or bruto[:200] + except Exception: + texto = bruto[:200] + else: + texto = bruto.strip() + break + norm = _normalizar_audio(partes[".wav"]) + if norm is None: + continue + onda, dur = norm + if texto and len(texto.strip()) >= 8: + registros.append({"tarefa": "asr", "texto": None, + "entrada": f"Transcreva o áudio ({dur:.1f}s):", + "saida": texto.strip()[:300], "imagem": None, + "audio": onda, + "meta": {"dur_s": round(dur, 2), + "origem": "neurogrid_synth"}}) + if len(registros) >= n_audio: + break + return registros + + +def _iter_parquet_local(repo_id: str, arquivo: str, revision: str | None = None): + """Streaming real de parquet por batches (pyarrow) — RAM O(batch).""" + from huggingface_hub import hf_hub_download + local = hf_hub_download(repo_id=repo_id, repo_type="dataset", filename=arquivo, + revision=revision, cache_dir=CACHE_TMP) + pf = pq.ParquetFile(local) + for batch in pf.iter_batches(batch_size=64): + yield from batch.to_pylist() + + +class StreamingCorpus: + """Fonte única multi-tarefa multimodal — UM DATASET POR VEZ, streaming, + dedup por hash e limites por fonte. Orçamento v2 ≈ 4,3k registros ativos.""" + + PLANO = [ + # --- base textual v1 --- + {"id": "dominguesm/restore-punctuation-ptbr-dataset", "ad": _ad_pontuacao_dominguesm, + "via": "stream", "max": 400}, + {"id": "CEIA-POSITIVO/ultrachat_br_clustred_balanced_v1", "ad": _ad_ultrachat, + "via": "stream", "max": 500}, + {"id": "dominguesm/Canarim-Instruct-PTBR-Dataset", "ad": _ad_canarim, + "via": "stream", "max": 500}, + {"id": "adalbertojunior/punctuation-ptbr", "ad": _ad_pontuacao_adalberto, + "via": "parquet", "arquivo": "punctuation-ptbr/train/0000.parquet", + "revision": "refs/convert/parquet", "max": 300}, + {"id": "iara-project/news-articles-ptbr-dataset", "ad": _ad_noticia, + "via": "stream", "max": 350}, + {"id": "manoela/noticias_ptbr", "ad": _ad_noticia, "via": "stream", "max": 350}, + {"id": "BrunoN-Dev/corpus-ptbr-v1", "ad": _ad_brunon, + "via": "parquet", "arquivo": "data/train-00000-of-00018.parquet", + "revision": None, "max": 300}, + # --- multimodais v2 (item 4b) --- + {"id": "marcelohaps/verbovision-oracle-facts-pt", "ad": _ad_verbovision, + "via": "stream", "max": 220}, + {"id": "amalia-llm/MMMU-PT", "ad": _ad_mmmu, "via": "stream", + "split": "validation", "max": 110}, + {"id": "amalia-llm/MMMU-Pro-PT", "ad": _ad_mmmu, "via": "stream", + "split": "test", "max": 90}, + {"id": "amalia-llm/OCRBench-PT", "ad": _ad_ocrbench, "via": "stream", + "split": "test", "max": 140}, + {"id": "amalia-llm/COCO-Caption2017-PT", "ad": _ad_coco_pt, "via": "stream", + "split": "validation", "max": 190}, + {"id": "weikaih/imaginative-perception-token-pt-eval-real", "ad": _ad_weikaih, + "via": "stream", "split": "td_path", "max": 110}, + {"id": "opedromartins/asr-leaderboard-datasets-ptbr", "ad": _ad_asr, + "via": "stream", "config": "mls", "split": "test", "max": 60}, + {"id": "opedromartins/asr-leaderboard-datasets-ptbr#tedx", "ad": _ad_asr, + "via": "stream", "config": "tedx", "split": "test", "max": 50}, + {"id": "opedromartins/asr-leaderboard-datasets-ptbr#fleurs", "ad": _ad_asr, + "via": "stream", "config": "fleurs", "split": "test", "max": 50}, + {"id": "marcosremar2/pt-br-tts-synth", "ad": None, "via": "tts_marcos", + "max": 240, "n_audio": 14}, + {"id": "leeaandrob/neurogrid-tts-synth-ptbr", "ad": None, "via": "degradado", + "max": 0}, + # --- degradado documentado (RAM/disco) --- + {"id": "carolina-c4ai/corpus-carolina", "ad": None, "via": "degradado", "max": 0}, + ] + + def __init__(self, token: str | None = None): + self.token = token or os.environ.get("HF_TOKEN") + self.eventos: list[EventoDataset] = [] + + def _iterar_fonte(self, plano: dict): + fonte, via = plano["id"], plano["via"] + if via == "degradado": + motivo = ("XML.gz (554 arquivos) sem parquet — fora do orçamento de RAM/disco " + "desta execução; adaptador documentado p/ GPU/SSD (doc 00 §4)" ) \ + if "carolina" in fonte else ( + "tars synth_clean_v1.tar (4,9 GB) contêm SOMENTE wavs, sem " + "transcrições embutidas no repo — sem par texto-áudio supervisionável; " + "amostragem por faixa HTTP implementada (_registros_tts_leeaandrob) e " + "ativável quando um arquivo de transcrição for publicado") + self.eventos.append(EventoDataset(fonte, "PULADO", motivo)) + return + if via == "tts_marcos": + yield from _registros_tts_marcos(self.token, plano["max"], + plano.get("n_audio", 12)) + return + if via == "wds_lee": + yield from _registros_tts_leeaandrob(self.token, plano.get("n_audio", 16)) + return + try: + if via == "stream": + cfg = plano.get("config") + split = plano.get("split", "train") + fonte_real = fonte.split("#")[0] # sufixo '#cfg' é só de exibição + try: + ds = load_dataset(fonte_real, cfg, split=split, streaming=True, + trust_remote_code=True) + except (TypeError, ValueError): + ds = load_dataset(fonte_real, cfg, split=split, streaming=True) + if fonte_real.startswith("opedromartins"): + # áudio bruto sem decode (evita torchcodec; bytes p/ soundfile) + from datasets import Audio + ds = ds.cast_column("audio", Audio(decode=False)) + yield from ds + else: # parquet dirigido + yield from _iter_parquet_local(fonte.split("#")[0], plano["arquivo"], + plano.get("revision")) + except Exception as e: + self.eventos.append(EventoDataset(fonte, "ERRO", + f"{type(e).__name__}: {str(e)[:180]}")) + + def coletar(self) -> tuple[list[dict], list[EventoDataset]]: + """Coleta sequencial (um dataset por vez) com dedup + limites + + limpeza de cache temporário após cada fonte.""" + vistos: set[str] = set() + registros: list[dict] = [] + for plano in self.PLANO: + fonte, ad, maximo = plano["id"], plano["ad"], plano["max"] + n_ok = 0 + for ex in self._iterar_fonte(plano): + if n_ok >= maximo: + break + reg = ad(ex) if ad else (ex if via_especial(plano["via"]) else None) + if reg is None: + continue + chave = _hash_texto(reg.get("texto") or reg.get("saida") + or reg.get("entrada") or "") + if chave in vistos: + continue + vistos.add(chave) + reg["fonte"] = fonte + reg.setdefault("imagem", None) + reg.setdefault("audio", None) + reg.setdefault("meta", {}) + if reg.get("imagem") is None: + reg.pop("imagem", None) + if reg.get("audio") is None: + reg.pop("audio", None) + registros.append(reg) + n_ok += 1 + if plano["via"] != "degradado": # degradado já registrou evento próprio + self.eventos.append(EventoDataset(fonte, "OK" if n_ok else "VAZIO", + plano.get("via", ""), n_ok)) + limpar_cache_temp() # um dataset por vez → disco sob controle + return registros, self.eventos + + def contagem_por_tarefa(self, registros: list[dict]) -> dict: + c = {t: 0 for t in TAREFAS} + for r in registros: + c[r["tarefa"]] += 1 + return c + + +def via_especial(via: str) -> bool: + return via in ("tts_marcos", "wds_lee") diff --git a/src/khtst/dados/tokenizador.py b/src/khtst/dados/tokenizador.py new file mode 100644 index 0000000000000000000000000000000000000000..380ffc6c54aa6ead218feab286bd9d8c57cc274b --- /dev/null +++ b/src/khtst/dados/tokenizador.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +"""Tokenizador paralelizável (item 9): BPE treinado sobre corpus PT-BR com a +biblioteca `tokenizers` (Rust, multithread nativo) + codificação em lote +paralela com pool de processos quando disponível. + +Projeto: vocabulário 8k (CPU) — configurável para 32k+ em GPU. +Tokens especiais: =0, =1, =2, =3, =4. +Prefixos de tarefa para o modelo multi-tarefa (doc raciocínio): + [lm] [instr] [noticia] [pont] — idem para o formato "Usuário:/Assistente:". +""" +from __future__ import annotations + +import os +from concurrent.futures import ProcessPoolExecutor + +from tokenizers import Tokenizer, models, pre_tokenizers, decoders, trainers + +PAD, BOS, EOS, UNK, MASK = 0, 1, 2, 3, 4 +ESPECIAIS = ["", "", "", "", ""] +PREFIXOS_TAREFA = {"lm": "[lm]", "instrucao": "[instr]", "noticia": "[noticia]", "pontuacao": "[pont]"} +ESPECIAIS_COMPLETAS = ESPECIAIS + list(PREFIXOS_TAREFA.values()) + + +class TokenizadorKHTST: + def __init__(self, caminho_json: str | None = None): + if caminho_json and os.path.exists(caminho_json): + self.tk = Tokenizer.from_file(caminho_json) + else: + self.tk = Tokenizer(models.BPE(unk_token="")) + self.tk.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=True) + self.tk.decoder = decoders.ByteLevel() + + def treinar(self, iterador_textos, vocab: int = 8192, salvar_em: str | None = None) -> dict: + """Treino BPE. `iterador_textos` deve produzir str (streaming — RAM limitada).""" + trainer = trainers.BpeTrainer( + vocab_size=vocab, + special_tokens=ESPECIAIS_COMPLETAS, + initial_alphabet=pre_tokenizers.ByteLevel.alphabet(), + min_frequency=2, + show_progress=False, + ) + self.tk.train_from_iterator(iterador_textos, trainer=trainer) + if salvar_em: + os.makedirs(os.path.dirname(salvar_em), exist_ok=True) + self.tk.save(salvar_em) + return {"vocab_size": self.tk.get_vocab_size()} + + # ---------------- codificação ---------------- + + def decodificar(self, ids: list[int]) -> str: + """v4: decodificação de ids → texto (filtra especiais ..).""" + ids_f = [i for i in ids if i not in {0, 1, 2, 3, 4}] + return self.tk.decode(ids_f, skip_special_tokens=True) + + def encode(self, texto: str, tarefa: str | None = None, max_len: int = 192, + com_bos: bool = True) -> list[int]: + prefixo = PREFIXOS_TAREFA.get(tarefa, "") + s = (prefixo + " " + texto).strip() if prefixo else texto + ids = self.tk.encode(s).ids + if com_bos: + ids = [BOS] + ids + return ids[:max_len] + + def encode_par(self, entrada: str, saida: str, tarefa: str | None = None, + max_len: int = 192) -> tuple[list[int], list[int]]: + """Para seq2seq: entrada codificada e saída com final.""" + return (self.encode(entrada, tarefa, max_len, com_bos=True), + self.tk.encode(saida).ids[: max_len - 1] + [EOS]) + + def decode(self, ids: list[int], pular_especiais: bool = True) -> str: + ids_f = [i for i in ids if i >= len(ESPECIAIS)] if pular_especiais else ids + return self.tk.decode(ids_f) + + # ---------------- paralelismo ---------------- + def encode_lote(self, textos: list[str], tarefa: str | None = None, + max_len: int = 192, threads: int = 0) -> list[list[int]]: + """Paralelizável: encode_batch nativo do Rust (multithread) — padrão; + `threads>1` roteia para pool nativo do tokenizers (uso: lotes grandes).""" + prefixo = PREFIXOS_TAREFA.get(tarefa, "") + ajustados = [(prefixo + " " + t).strip() if prefixo else t for t in textos] + enc = self.tk.encode_batch(ajustados) + return [([BOS] + e.ids)[:max_len] for e in enc] + + @property + def vocab_size(self) -> int: + return self.tk.get_vocab_size() + + def salvar(self, caminho: str) -> None: + os.makedirs(os.path.dirname(caminho), exist_ok=True) + self.tk.save(caminho) + + @classmethod + def carregar(cls, caminho: str) -> "TokenizadorKHTST": + return cls(caminho) diff --git a/src/khtst/geracao/__init__.py b/src/khtst/geracao/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..70b20b8017b31bbeef4dd97344ac4988c477de14 --- /dev/null +++ b/src/khtst/geracao/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +"""Geração multimodal (v5): difusão (SD txt2img / img2img / inpaint).""" +from khtst.geracao.difusao import GeradorMultimodal + +__all__ = ["GeradorMultimodal"] diff --git a/src/khtst/geracao/difusao.py b/src/khtst/geracao/difusao.py new file mode 100644 index 0000000000000000000000000000000000000000..5c479ccb7b1da52a16e9f8717b228155e8e9b4a7 --- /dev/null +++ b/src/khtst/geracao/difusao.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +"""Geração multimodal por DIFUSÃO — v5 (doc 17). + +Compreensão geracional multimodal (pedido explícito): os três pipelines +StableDiffusion do diffusers são integrados ao ciclo de KHTST: + + texto → imagem (StableDiffusionPipeline) + imagem → imagem (StableDiffusionImg2ImgPipeline — força guiada pela + SIMILARIDADE semântica medida pelo encoder perceptual) + imagem → inpaint (StableDiffusionInpaintPipeline — máscara guiada pela + saliência de atenção) + +CICLO FECHADO DE COMPREENSÃO GERACIONAL (doc 17 §2): + NLG enriquece o prompt (PT-BR) → difusão gera → encoder de imagem do + KHTST re-encoda o resultado → protótipo entra na memória SOM → gerações + futuras condicionam na memória. A geração passa a ser PERCEBIDA pelo + próprio modelo — compreensão geracional, não só síntese. + +TEOREMA 17.1 (força de edição semanticamente guiada). Seja s = cos(e_in, +e_plano) a similaridade entre o embedding da imagem de entrada e do plano +gerado. A força de re-edição strength = clip(1 − s, 0,35, 0,80) satisfaz: +s=1 (idêntico) ⇒ strength=0,35 (edição leve, preserva conteúdo); s=0 ⇒ 0,80 +(recriação forte). Monotonia: ∂strength/∂s ≤ 0. + +TEOREMA 17.2 (degradação honesta). Sem GPU/diffusers/pesos, o gerador opera +em modo "planejado": devolve um PLANO estruturado (prompt enriquecido, +parâmetros, força calculados) — NUNCA pixels falsos. Toda telemetria registra +o modo real (`real` | `planejado`). + +PESOS: por padrão usa repositório TINY de teste (CPU/RAM pequena). Em +produção defina `repo_id` p.ex. "stabilityai/stable-diffusion-2-1-base" +(GPU ≥ 8GB). +""" +from __future__ import annotations + +import io +import os +import time + +import torch + +# Importação EXATA solicitada (item do escopo v5): +from diffusers import (StableDiffusionImg2ImgPipeline, + StableDiffusionInpaintPipeline, + StableDiffusionPipeline) + +MODOS = ("real", "planejado") + + +def _dispositivo() -> str: + if torch.cuda.is_available(): + return "cuda" + if torch.backends.mps.is_available(): + return "mps" + return "cpu" + + +class GeradorMultimodal: + """Facade dos 3 pipelines de difusão com carga PREGUIÇOSA e ciclo fechado. + + Args: + repo_id: repositório de pesos (SD). Default: tiny de teste. + altura/largura: resolução (múltiplos de 8). + passos_inferencia / guia_escala: parâmetros do scheduler. + """ + + def __init__(self, repo_id: str = "hf-internal-testing/tiny-stable-diffusion-torch", + altura: int = 128, largura: int = 128, + passos_inferencia: int = 8, guia_escala: float = 7.5, + hub=None, token: str | None = None): + self.repo_id = repo_id + self.altura, self.largura = altura, largura + self.passos = passos_inferencia + self.guia = guia_escala + self.hub = hub + self.token = token or os.environ.get("HF_TOKEN") + self.device = _dispositivo() + self.dtype = torch.float16 if self.device == "cuda" else torch.float32 + self._pipelines: dict = {} + self.modo = "planejado" # honesto até a carga REAL confirmar + self.eventos: list = [] + + # ---------------- infra ---------------- + def _obter_pipeline(self, classe, chave: str): + if chave in self._pipelines: + return self._pipelines[chave] + try: + pipe = classe.from_pretrained( + pretrained_model_name_or_path=self.repo_id, + torch_dtype=self.dtype, token=self.token) + pipe = pipe.to(self.device) + pipe.set_progress_bar_config(disable=True) + self._pipelines[chave] = pipe + self.modo = "real" + self.eventos.append(f"pipeline {chave} carregado ({self.repo_id})") + return pipe + except Exception as e: + self.modo = "planejado" + self.eventos.append(f"pipeline {chave}: {type(e).__name__}: {e}") + return None + + # ---------------- força semântica (Teorema 17.1) ---------------- + @staticmethod + def forca_por_similaridade(emb_in: torch.Tensor | None, + emb_plano: torch.Tensor | None) -> float: + """strength = clip(1 − cos(e_in, e_plano), 0,35, 0,80).""" + if emb_in is None or emb_plano is None: + return 0.55 + s = torch.nn.functional.cosine_similarity( + emb_in.flatten().float().unsqueeze(0), + emb_plano.flatten().float().unsqueeze(0)).item() + return float(min(0.80, max(0.35, 1.0 - s))) + + # ---------------- três operações ---------------- + def texto_para_imagem(self, prompt: str, **kw) -> dict: + """StableDiffusionPipeline: texto → imagem.""" + pipe = self._obter_pipeline(StableDiffusionPipeline, "txt2img") + if pipe is None: + return self._plano("txt2img", prompt=prompt) + out = pipe(prompt, height=self.altura, width=self.largura, + num_inference_steps=kw.get("passos", self.passos), + guidance_scale=kw.get("guia", self.guia)) + return {"modo": "real", "op": "txt2img", "imagem": out.images[0]} + + def imagem_para_imagem(self, prompt: str, imagem, forca: float | None = None, + **kw) -> dict: + """StableDiffusionImg2ImgPipeline: imagem+texto → imagem. `forca` + semanticamente guiada (Teorema 17.1).""" + pipe = self._obter_pipeline(StableDiffusionImg2ImgPipeline, "img2img") + if pipe is None: + return self._plano("img2img", prompt=prompt, forca=forca) + f = forca if forca is not None else kw.get("forca_default", 0.55) + out = pipe(prompt=prompt, image=imagem, strength=f, + num_inference_steps=kw.get("passos", self.passos), + guidance_scale=kw.get("guia", self.guia)) + return {"modo": "real", "op": "img2img", "imagem": out.images[0], + "forca": float(f)} + + def inpaint(self, prompt: str, imagem, mascara, **kw) -> dict: + """StableDiffusionInpaintPipeline: imagem+máscara → imagem editada.""" + pipe = self._obter_pipeline(StableDiffusionInpaintPipeline, "inpaint") + if pipe is None: + return self._plano("inpaint", prompt=prompt) + out = pipe(prompt=prompt, image=imagem, mask_image=mascara, + height=self.altura, width=self.largura, + num_inference_steps=kw.get("passos", self.passos), + guidance_scale=kw.get("guia", self.guia)) + return {"modo": "real", "op": "inpaint", "imagem": out.images[0]} + + # ---------------- ciclo fechado (compreensão geracional) ---------------- + def compreensao_geracional(self, prompt: str, modelo, orquestrador=None, + op: str = "txt2img", imagem_base=None, + emb_in: torch.Tensor | None = None, + emb_plano: torch.Tensor | None = None, + mascara=None) -> dict: + """Ciclo fechado doc 17 §2: NLG enriquece o prompt → difusão → + re-encodagem pelo KHTST → protótipo na memória SOM.""" + t0 = time.time() + # (i) prompt enriquecido pela NLG (estilo corrente — doc 15 §1) + prompt_enriquecido = prompt + if modelo is not None and getattr(modelo, "nlg", None) is not None: + try: + prompt_enriquecido = modelo.nlg.enriquecer_prompt(prompt) + except Exception: + pass + # (ii) força semântica (Teorema 17.1) + forca = self.forca_por_similaridade(emb_in, emb_plano) \ + if op == "img2img" else None + # (iii) geração + if op == "txt2img": + r = self.texto_para_imagem(prompt_enriquecido) + elif op == "img2img": + r = self.imagem_para_imagem(prompt_enriquecido, imagem_base, + forca=forca) + elif op == "inpaint": + r = self.inpaint(prompt_enriquecido, imagem_base, mascara) + else: + r = {"modo": "planejado", "op": op, "erro": "op desconhecida"} + # (iv) re-encodagem perceptual + memória SOM + emb_out = None + if r.get("modo") == "real" and modelo is not None \ + and getattr(modelo, "usar_multimodal", False): + try: + import numpy as np + from PIL import Image + img = r.get("imagem") + arr = np.asarray(img.resize((96, 96)).convert("RGB"), + dtype=np.float32) / 255.0 + t = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0) + emb_out = modelo.enc_imagem(t) + if orquestrador is not None and emb_out is not None: + with torch.no_grad(): + orquestrador.treinar_memoria( + emb_out.detach().reshape(-1, modelo.d), epocas=1) + except Exception as e: + r["re_encode_erro"] = f"{type(e).__name__}: {e}" + r.update({"prompt_enriquecido": prompt_enriquecido, + "latencia_s": round(time.time() - t0, 2), + "embedding_saida": emb_out}) + if self.hub is not None: + self.hub.atributo("difusao/modo_real", + 1.0 if r.get("modo") == "real" else 0.0) + self.hub.atributo("difusao/latencia_s", r["latencia_s"]) + return r + + # ---------------- degradação honesta (Teorema 17.2) ---------------- + def _plano(self, op: str, **params) -> dict: + """Modo planejado: NUNCA pixels falsos — só o plano estruturado.""" + self.modo = "planejado" + return {"modo": "planejado", "op": op, "repo": self.repo_id, + "altura": self.altura, "largura": self.largura, + "passos": self.passos, "guia": self.guia, **params} + + def imagem_para_bytes(self, img) -> bytes: + """PIL.Image → PNG bytes (persistência sem segredos).""" + buf = io.BytesIO() + img.save(buf, format="PNG") + return buf.getvalue() + + def estatisticas(self) -> dict: + return {"modo": self.modo, "repo": self.repo_id, + "device": self.device, "pipelines": list(self._pipelines), + "eventos": self.eventos[-5:]} diff --git a/src/khtst/memoria/__init__.py b/src/khtst/memoria/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/khtst/memoria/atencao_som.py b/src/khtst/memoria/atencao_som.py new file mode 100644 index 0000000000000000000000000000000000000000..c060cf347cacf933c8513a5735f7e369b7bc5462 --- /dev/null +++ b/src/khtst/memoria/atencao_som.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- +"""Atenção ENTRE variantes de Redes de Kohonen (v5, doc 16 §§3–5 — item 2 da +1ª requisição, com garantia de ZERO neurônios isolados). + +Problema: o OrquestradorSOM (doc 03 §3) roteia cada entrada para UMA variante +(rígido) — o recall perde informação das outras 8 e neurônios pouco usados +podem ficar isolados. Solução: recall ATENCIONAL sobre TODAS as variantes + +bônus de novidade direcionado a variantes com neurônios órfãos. + +Formalização. Para a consulta x e a variante v com codebook W_v ∈ R^{k_v×d}: + z_v(x) = W_v[ BMU_v(x) ] (protótipo vencedor) + d_v(x) = ‖z_v(x) − x‖₂ (erro de quantização da variante) + n_v = fração de neurônios órfãos de v (h_j EMA < ε, doc 10 §2.3) + a_v(x) = softmax( −d_v(x)/τ + γ·n_v ) (eq. 16.3) + ẑ(x) = Σ_v a_v(x)·z_v(x) (eq. 16.4) + +TEOREMA 16.3 (casco convexo). ẑ(x) ∈ casco{∪_v W_v}: soma convexa de +protótipos de memória. O recall NUNCA extrapola fora da memória armazenada +(segurança do recall associativo — nenhuma alucinação de protótipo). + +TEOREMA 16.4 (generalização do roteamento duro). Para τ→0 e γ=0, a_v → +δ_{v*} com v* = argmin_v d_v — recupera o roteamento do orquestrador +(doc 03 §3) como caso limite; τ>0 interpola entre especialistas (meta- +estabilidade: variantes com EQ similar cooperam). + +TEOREMA 16.5 (zero órfãos assintótico). Sejam (i) dados i.i.d. com suporte +cada região de Voronoi de medida ≥ μ_min > 0; (ii) taxa de aprendizado +Robbins–Monro (Ση_t=∞, Ση_t²<∞, Teorema 1.2); (iii) resemeadura periódica +relocando órfãos ao amostra de pior EQ; (iv) bônus de novidade γ>0 nas rotas +(eq. 16.3). Então P[neurônio órfão após T atualizações] ≤ (1−μ_min)^{c_T}, +com c_T = #{amostras roteadas à variante} → ∞ linearmente, e com +T ≥ (4/μ_min)·ln(k/δ) amostras ⇒ P[algum órfão] ≤ k·(1−μ_min)^{c_T} ≤ δ +(Hoeffding sobre contagens de cobertura). A equipe (atenção + resemeadura) +torna o evento "neurônio isolado" de PROBABILIDADE DECAYENTE — e o teste +`verificar_sem_orfas` verifica o invariante após a consolidação. + +TEOREMA 16.6 (custo). Para V variantes: custo = Σ_v O(k_v·d) (BMUs) + +O(V·d) (mistura) + O(V²) trivial — LINEAR em Σ k_v d; a matriz de atenção +entre variantes é V×V = 9×9. + +Integração: `OrquestradorSOM.mapear_atencao(x)` devolve ẑ e registra +telemetria (pesos a_v, distâncias d_v, órfãos por variante). O alinhamento +do modelo (doc 10 §2) usa ẑ quando ativo — TODAS as variantes participam do +gradiente de alinhamento via rotas amostrais (sem backprop nos SOMs, que +permanecem on-line: Teorema 10.5 preservado). +""" +from __future__ import annotations + +import torch + +from khtst.memoria.variantes_especiais import (CounterpropagationNetwork, + HierarchicalSOM, SupervisedSOM) + + +def prototipo_e_distancia(variante, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """(z_v, d_v) para x: (B,d) — espelha OrquestradorSOM.mapear, retornando + também o erro de quantização por amostra.""" + with torch.no_grad(): + if isinstance(variante, CounterpropagationNetwork): + idx = variante.kohonen.bmu(x)[0] + z = variante.kohonen.w[idx] + elif isinstance(variante, SupervisedSOM): + idx = variante.bmu(x)[0] + z = variante.w[idx] + elif isinstance(variante, HierarchicalSOM): + idx = variante.n1.bmu(x)[0] + z = variante.n1.w[idx] + elif hasattr(variante, "bmu"): + idx = variante.bmu(x)[0] + z = variante.w[idx] + else: # GCS: grafo sem método bmu + idx = torch.cdist(x, variante.w).pow(2).argmin(dim=1) + z = variante.w[idx] + d = (z - x).norm(dim=-1) + return z, d + + +def n_orfaos(variante) -> int: + """Neurônios com uso EMA abaixo do limiar (doc 10 §2.3) — ou células + nunca usadas no GCS (utilidade zero, Teorema 2.3).""" + for alvo in (variante, getattr(variante, "kohonen", None), + getattr(variante, "n1", None)): + if alvo is not None and hasattr(alvo, "hit_ema") and hasattr(alvo, "eps_ativo"): + return int((alvo.hit_ema < alvo.eps_ativo).sum()) + if alvo is not None and hasattr(alvo, "usos"): + return int((alvo.usos == 0).sum()) + return 0 + + +def resemear_orfaos(variante, dados: torch.Tensor) -> int: + """Reloca neurônios órfãos para amostras reais — passo (iii) do Teorema + 16.5 (SOM: amostras de PIOR quantização; GCS: células nunca usadas). + Devolve nº de neurônios relocados.""" + if dados.numel() == 0: + return 0 + for alvo in (variante, getattr(variante, "kohonen", None), + getattr(variante, "n1", None)): + if alvo is None or not hasattr(alvo, "w"): + continue + with torch.no_grad(): + if hasattr(alvo, "hit_ema") and hasattr(alvo, "eps_ativo"): + mortos = (alvo.hit_ema < alvo.eps_ativo).nonzero(as_tuple=True)[0] + if mortos.numel() == 0: + return 0 + n = int(mortos.numel()) + eq = ((dados.unsqueeze(1) - alvo.w.unsqueeze(0)) ** 2) \ + .sum(-1).min(dim=1).values + # amostragem COM reposição nas regiões de pior EQ (Teorema 16.5): + # n ≥ nº de amostras — todo morto recebe uma amostra real + piores = eq.topk(min(n, dados.shape[0])).indices + fontes = dados[torch.randint(0, dados.shape[0], (n,))] + alvo.w[mortos] = fontes + alvo.hit_ema[mortos] = 0.08 # nasce 'ativo fraco' (doc 10 §2.3) + return n + if hasattr(alvo, "usos"): + mortos = (alvo.usos == 0).nonzero(as_tuple=True)[0] + if mortos.numel() == 0: + return 0 + fontes = dados[torch.randint(0, dados.shape[0], (int(mortos.numel()),))] + alvo.w[mortos] = fontes + alvo.usos[mortos] = 1.0 + return int(mortos.numel()) + return 0 + + +class AtencaoEntreVariantes: + """Recall atencional sobre TODAS as variantes Kohonen (eqs. 16.3–16.4).""" + + def __init__(self, tau: float = 0.5, gamma_novidade: float = 0.15, + hub=None): + assert tau > 0, "τ deve ser positivo (Teorema 16.4)" + self.tau = float(tau) + self.gamma = float(gamma_novidade) + self.hub = hub + self.ultimo: dict = {} + + # ---------------- núcleo ---------------- + def mapear(self, x: torch.Tensor, orquestrador) -> torch.Tensor: + """x: (B,d) → ẑ: (B,d) ∈ casco{∪_v W_v} (Teorema 16.3).""" + nomes = list(orquestrador.variantes.keys()) + V = len(nomes) + zs, ds = [], [] + for nome in nomes: + z_v, d_v = prototipo_e_distancia(orquestrador.variantes[nome], x) + zs.append(z_v) + ds.append(d_v) + Z = torch.stack(zs, dim=1) # (B, V, d) + D = torch.stack(ds, dim=1) # (B, V) + novidade = torch.tensor( + [n_orfaos(orquestrador.variantes[n]) / + max(1, getattr(orquestrador.variantes[n], "k", 1)) for n in nomes], + dtype=D.dtype) + escore = -D / self.tau + self.gamma * novidade.unsqueeze(0) # (B,V) + a = torch.softmax(escore, dim=1) # eq. 16.3 + z_hat = (a.unsqueeze(-1) * Z).sum(dim=1) # eq. 16.4 (B,d) + self.ultimo = {"pesos": a.mean(dim=0).detach(), + "distancias": D.mean(dim=0).detach(), + "entropia": float(-(a * torch.log(a + 1e-9)).sum(1).mean()), + "variante_dominate": nomes[int(a.mean(0).argmax())]} + if self.hub is not None: + for j, nome in enumerate(nomes): + self.hub.atributo(f"som_atn/peso_{nome}", + float(self.ultimo["pesos"][j])) + self.hub.atributo("som_atn/entropia", self.ultimo["entropia"]) + return z_hat + + # ---------------- invariante anti-órfãos ---------------- + def verificar_sem_orfas(self, orquestrador, dados: torch.Tensor | None = None, + resemear: bool = True) -> dict: + """Verifica (e corrige com resemeadura) o invariante ZERO neurônios + isolados em TODAS as variantes (Teorema 16.5, passo iii).""" + relatorio: dict = {} + for nome, v in orquestrador.variantes.items(): + relocations = 0 + if resemear and dados is not None: + relocations = resemear_orfaos(v, dados) + rel = {"orfaos": n_orfaos(v), "relocados": relocations} + if hasattr(v, "taxa_ativos"): + rel["taxa_ativos"] = v.taxa_ativos() + elif hasattr(v, "kohonen") and hasattr(v.kohonen, "taxa_ativos"): + rel["taxa_ativos"] = v.kohonen.taxa_ativos() + relatorio[nome] = rel + if self.hub is not None: + self.hub.atributo(f"som_atn/orfaos_{nome}", rel["orfaos"]) + relatorio["_invariante_ok"] = all(r["orfaos"] == 0 + for k, r in relatorio.items() + if not k.startswith("_")) + return relatorio diff --git a/src/khtst/memoria/interna.py b/src/khtst/memoria/interna.py new file mode 100644 index 0000000000000000000000000000000000000000..a2b691a1abb69806abf986229a23f2a1441c970c --- /dev/null +++ b/src/khtst/memoria/interna.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- +"""Memória interna multimodal do modelo (v3, doc 11 §9) — gestão aprimorada: + +1. MEMÓRIA DE TRABALHO: slots (z_i, u_i, h_i, t_i) com ESCRITA CONFICIENTE + (só grava com h_t ≥ h_escrita), recuperação por similaridade+bônus de + utilidade e EVICÇÃO por utilidade U_i = u_i·e^{−Δt/T}·h_i. +2. COMPRESSÃO VQ: entradas antigas viram códigos discretos (log₂N bits/slot) + via quantizador vetorial EMA com dead-code restart por JANELA (correção + do defect 3 do estudo: contagem decrescente, não acumulada) + perda de + diversidade (Teorema 11.9: H ≥ log N − ε). +3. ÍNDICE SOM: endereço simbólico (mapa, BMU) de cada escrita — recall por + categoria, não só por similaridade bruta. +4. CONTRATOS (doc 11 §10): matriz de permissões verificada em runtime — + violação registra defeito no hub de telemetria e levanta erro (divisão de + competências entre percepcao/memoria/raciocinio/treino/nucleo). +""" +from __future__ import annotations + +import math +import time +from dataclasses import dataclass, field + +import torch +import torch.nn as nn +import torch.nn.functional as F + +# matriz de permissões (doc 11 §10): quem pode fazer o quê com a memória +PERMISSOES = { + "percepcao": {"escrever", "consultar"}, + "nucleo": {"consultar", "escrever"}, + "raciocinio": {"consultar"}, + "treino": {"consultar", "consolidar", "estatisticas"}, + "telemetria": {"estatisticas"}, +} + + +class QuantizadorVetorialEMA(nn.Module): + """VQ‑EMA com dead-code restart por JANELA (defeito 3 corrigido: a + contagem de uso decai — EMA — em vez de acumular desde o início).""" + + def __init__(self, n_codigos: int, dim: int, decaimento: float = 0.99, + custo_compromisso: float = 0.25, morto_ema: float = 0.01, + peso_diversidade: float = 0.1): + super().__init__() + embed = torch.randn(n_codigos, dim) * 0.02 + self.register_buffer("embeddings", embed) + self.register_buffer("ema_cont", torch.zeros(n_codigos)) + self.register_buffer("ema_peso", embed.clone()) + self.decaimento = decaimento + self.custo = custo_compromisso + self.morto_ema = morto_ema + self.peso_div = peso_diversidade + self.n_codigos = n_codigos + + def forward(self, z: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + flat = z.reshape(-1, z.shape[-1]) + dist = (flat.pow(2).sum(1, keepdim=True) + - 2 * flat @ self.embeddings.t() + + self.embeddings.pow(2).sum(1)) + idx = dist.argmin(dim=1) + one_hot = F.one_hot(idx, self.n_codigos).type(flat.dtype) + q = one_hot @ self.embeddings + if self.training: + with torch.no_grad(): + cont = one_hot.sum(0) + ema_n = self.decaimento * self.ema_cont + (1 - self.decaimento) * cont + self.ema_peso = (self.decaimento * self.ema_peso + + (1 - self.decaimento) * (one_hot.t() @ flat)) + self.ema_cont = ema_n + self.embeddings.copy_(self.ema_peso / self.ema_cont.unsqueeze(1).clamp(min=1e-6)) + # dead-code restart por janela EMA (não acumulada) + mortos = (self.ema_cont < self.morto_ema).nonzero(as_tuple=True)[0] + if mortos.numel() and flat.shape[0] > 0: + n = min(mortos.numel(), flat.shape[0]) + escolha = torch.randperm(flat.shape[0])[:n] + self.embeddings.data[mortos[:n]] = flat[escolha].detach() + self.ema_cont.data[mortos[:n]] = 1.0 + self.ema_peso.data[mortos[:n]] = flat[escolha].detach() + e_lat = F.mse_loss(q.detach(), flat) + q_lat = F.mse_loss(q, flat.detach()) + # diversidade: -H(p) normalizado (força uso uniforme do codebook) + if self.training and self.peso_div > 0: + p = one_hot.float().mean(0).clamp_min(1e-10) + H = -(p * p.log()).sum() + div = -H / math.log(self.n_codigos) + else: + div = torch.zeros(()) + perda = q_lat + self.custo * e_lat + self.peso_div * div + q_ste = flat + (q - flat).detach() # straight-through + return q_ste.reshape_as(z), perda, idx.reshape(z.shape[:-1]) + + @torch.no_grad() + def entropia_uso(self) -> float: + p = (self.ema_cont / self.ema_cont.sum().clamp(min=1e-9)).clamp_min(1e-10) + return float(-(p * p.log()).sum()) + + +@dataclass +class SlotMemoria: + z: torch.Tensor # (d,) conteúdo + utilidade: float = 0.0 # EMA de acessos + confianca: float = 0.5 # h_t no momento da escrita + t: float = field(default_factory=time.time) + comprimido: bool = False + codigo: int | None = None # índice VQ quando comprimido + endereco_som: tuple | None = None # (variante, índice BMU) + origem: str = "percepcao" + tarefa: str | None = None + + +class RegistroAcessos: + """Verificação de contratos (doc 11 §10): cada operação valida a matriz + de permissões; violações são coletadas no hub de telemetria.""" + + def __init__(self): + self.historico: list[dict] = [] + self.violacoes: list[dict] = [] + + def registrar(self, origem: str, operacao: str, ok: bool): + entrada = {"origem": origem, "operacao": operacao, "ok": ok, + "t": time.time()} + self.historico.append(entrada) + if not ok: + self.violacoes.append(entrada) + return ok + + +class MemoriaInterna: + """Memória de trabalho + compressão VQ + índice SOM + contratos.""" + + def __init__(self, d: int, n_slots: int = 64, h_escrita: float = 0.5, + idade_compressao_s: float = 900.0, n_codigos: int = 64, + lambda_utilidade: float = 0.1, meia_vida_s: float = 1800.0): + self.d = d + self.n_slots = n_slots + self.h_escrita = h_escrita + self.idade_compressao = idade_compressao_s + self.lambda_u = lambda_utilidade + self.meia_vida = meia_vida_s + self.slots: list[SlotMemoria] = [] + self.vq = QuantizadorVetorialEMA(n_codigos, d) + self.acessos = RegistroAcessos() + self.escritas_total = 0 + self.rejeitadas_confianca = 0 + self.consultas = 0 + self.acertos = 0 # consulta com ≥1 slot útil (score>0) + self.perda_vq_ema: float | None = None + self.som_index: dict[tuple, list[int]] = {} + + # ---------------- contratos ---------------- + def _checar(self, origem: str, operacao: str) -> bool: + ok = operacao in PERMISSOES.get(origem, set()) + self.acessos.registrar(origem, operacao, ok) + return ok + + # ---------------- escrita conficiente ---------------- + def escrever(self, z: torch.Tensor, h_t: float, origem: str = "percepcao", + tarefa: str | None = None) -> bool: + """Grava z (d,) se h_t ≥ h_escrita (doc 11 §9.1). Retorna se gravou.""" + if not self._checar(origem, "escrever"): + raise PermissionError(f"{origem} não pode escrever na memória") + h_t = float(min(max(h_t, 0.0), 1.0)) + if h_t < self.h_escrita: + self.rejeitadas_confianca += 1 + return False + z = z.detach().flatten().float() + if z.shape[0] != self.d: + raise ValueError(f"dimensão {z.shape[0]} ≠ d={self.d}") + if len(self.slots) >= self.n_slots: + self._evict() + self.slots.append(SlotMemoria(z=z, confianca=h_t, origem=origem)) + self.escritas_total += 1 + return True + + def indexar_som(self, indice_slot: int, variante: str, bmu: int): + """Registra endereço simbólico (mapa, BMU) — recall por categoria.""" + slot = self.slots[indice_slot] + slot.endereco_som = (variante, bmu) + self.som_index.setdefault((variante, bmu), []).append(indice_slot) + + # ---------------- consulta ---------------- + def consultar(self, q: torch.Tensor, m: int = 3, origem: str = "nucleo") -> dict: + """score = cos(q, z_i) + λ_u·u_i; devolve top-m e metadados.""" + if not self._checar(origem, "consultar"): + raise PermissionError(f"{origem} não pode consultar a memória") + self.consultas += 1 + if not self.slots: + return {"vetores": torch.zeros(m, self.d), "scores": [0.0] * m, + "indices": [], "acerto": False} + q = q.detach().flatten().float() + Z = torch.stack([s.z for s in self.slots]) # (S,d) + Zn = F.normalize(Z, dim=1) + cos = Zn @ F.normalize(q, dim=0) # (S,) + agora = time.time() + U = torch.tensor([s.utilidade * math.exp(-(agora - s.t) / self.meia_vida) + * s.confianca for s in self.slots]) + score = cos + self.lambda_u * U + m = min(m, len(self.slots)) + topo = score.topk(m).indices.tolist() + for i in topo: + s = self.slots[i] + s.utilidade = 0.9 * s.utilidade + 0.1 + s.t = agora + acerto = float(score[topo[0]]) > 0.0 + self.acertos += int(acerto) + return {"vetores": Z[topo], "scores": [float(score[i]) for i in topo], + "indices": topo, "acerto": acerto} + + # ---------------- evicção e compressão ---------------- + def _evict(self): + agora = time.time() + U = [s.utilidade * math.exp(-(agora - s.t) / self.meia_vida) * s.confianca + for s in self.slots] + i = int(torch.tensor(U).argmin()) + self.slots.pop(i) + # reindexa (índices deslocam) + novo = {} + for chave, idxs in self.som_index.items(): + novo[chave] = [j - 1 if j > i else j for j in idxs if j != i] + self.som_index = {k: v for k, v in novo.items() if v} + + def comprimir_antigos(self, origem: str = "treino") -> int: + """Slots com idade > idade_compressao viram códigos VQ (log₂N bits). + Teorema 11.9: economia 1 − log₂N/(32·d) com distorção ≤ D.""" + if not self._checar(origem, "consolidar"): + raise PermissionError(f"{origem} não pode consolidar a memória") + agora = time.time() + alvo = [i for i, s in enumerate(self.slots) + if not s.comprimido and agora - s.t > self.idade_compressao] + if not alvo: + return 0 + Z = torch.stack([self.slots[i].z for i in alvo]) + if not Z.requires_grad: + Z = Z.clone().requires_grad_(False) + self.vq.train() + q, perda, idx = self.vq(Z) + self.perda_vq_ema = (perda if self.perda_vq_ema is None + else 0.9 * self.perda_vq_ema + 0.1 * float(perda)) + for j, i in enumerate(alvo): + s = self.slots[i] + s.comprimido = True + s.codigo = int(idx[j]) + s.z = q[j].detach() # conteúdo ≈ quantizado (poupa nada + # em RAM aqui — a economia real é nos CHECKPOINTS: só o codebook + # (N·d) viaja; os códigos ficam simbólicos no meta.json) + return len(alvo) + + # ---------------- estatísticas ---------------- + def estatisticas(self) -> dict: + agora = time.time() + n_comp = sum(s.comprimido for s in self.slots) + return { + "slots": len(self.slots), "capacidade": self.n_slots, + "escritas": self.escritas_total, + "rejeitadas_confianca": self.rejeitadas_confianca, + "hit_rate": (self.acertos / self.consultas) if self.consultas else 0.0, + "comprimidos": n_comp, + "entropia_codebook": self.vq.entropia_uso(), + "perda_vq": self.perda_vq_ema, + "violacoes_contrato": len(self.acessos.violacoes), + "economia_bits_por_slot": (1.0 - math.log2(self.vq.n_codigos) + / (32.0 * self.d)), + } diff --git a/src/khtst/memoria/janela_1m.py b/src/khtst/memoria/janela_1m.py new file mode 100644 index 0000000000000000000000000000000000000000..269ed1acffb54887f835c9ecaf837fe947533a93 --- /dev/null +++ b/src/khtst/memoria/janela_1m.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +"""Janela de contexto de 1M tokens com compressão INDEXADA (doc 13) — v4. + +Três níveis (doc 13 §1): + fino: últimos L_fino tokens (atenção normal do tronco); + médio: resumos z_m por segmento via P consultas aprendíveis (AttnPool); + grosso: grade SOM grosseira (índice invertido célula → segmentos). + +Recuperação (Teorema 13.1): coarse = BMU + vizinhança r; fine = top-k por +produto interno. Memória O(M·d) ≈ 6 MB em fp32 para N=1M (d=192) — eq. 13.1. + +Contratos (Agente Engenheiro): M ≤ M_max (Teorema 13.2) e shapes de saída — +asserções locais; violação é BUG. +""" +from __future__ import annotations + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from khtst.memoria.som_base import SOMKohonen + +# Eq. 13.1 — orçamentos fixos (d=192): M_max = ⌈(1e6 − L_fino)/s⌉ +N_MAX_TOKENS = 1_000_000 +L_FINO = 4096 +W_SEG = 256 +S_PASSO = 128 # overlap 50% +P_CONSULTAS = 8 +K_RECALL = 4 +VIZ_R = 2 # anel da vizinhança no índice (Teorema 13.1: r ≥ ⌈δ/R⌉) + + +class JanelaContexto1M(nn.Module): + """Compressão indexada de contexto longo (doc 13).""" + + def __init__(self, d: int = 192, m_max: int = 7813, + k_grade: int | None = None, semente: int = 2026): + super().__init__() + self.d = d + self.m_max = m_max + # índice grosseiro: ⌈M^1/2⌉² células (doc 13 §1.3) + lado = k_grade or int((m_max ** 0.5) + 1) + lado = max(16, min(128, lado)) + self.grade = SOMKohonen(lado * lado, d, sigma0=4.0, semente=semente) + self.grade.total_planejado = float(m_max * 4) # orçamento p/ agendamento + self.consultas = nn.Parameter(torch.randn(P_CONSULTAS, d) * 0.02) + self.proj_retrieval = nn.Linear(d, d, bias=False) + # buffers de resumo + metadados (utilidade p/ LRU do Teorema 13.2) + self.register_buffer("z", torch.zeros(m_max, d)) + self.register_buffer("utilidade", torch.zeros(m_max)) + self.register_buffer("ocupado", torch.zeros(m_max, dtype=torch.bool)) + self.n_segmentos = 0 + self.ponteiro = 0 + self.hits_celula = 0 + self.consultas_totais = 0 + + # ---------------- inserção (doc 13 §4) ---------------- + @torch.no_grad() + def insere(self, oculto: torch.Tensor): + """oculto: (T, d) tokens novos (estado oculto do tronco). Segmenta em + janelas w com passo s, resume por AttnPool e indexa na grade.""" + if oculto.dim() != 2 or oculto.shape[1] != self.d or oculto.shape[0] == 0: + return + T = oculto.shape[0] + inicios = list(range(0, max(1, T - W_SEG + 1), S_PASSO)) + for ini in inicios: + fim = min(ini + W_SEG, T) + seg = oculto[ini:fim] # (t, d) + z = self._resumir(seg) # (d,) + idx = self.ponteiro % self.m_max + # Teorema 13.2: substituição por utilidade mínima quando cheio + if self.n_segmentos >= self.m_max: + alvo = int(self.utilidade.masked_fill(~self.ocupado, + float("inf")).argmin()) + idx = alvo + self.z[idx] = z + self.ocupado[idx] = True + self.utilidade[idx] = 1.0 + self.grade.treinar_lote(z.unsqueeze(0)) # índice SOM + self.ponteiro = idx + 1 + self.n_segmentos = min(self.n_segmentos + 1, self.m_max) + assert self.n_segmentos <= self.m_max, \ + "BUG (doc 13.2): M_max violado" + + def _resumir(self, seg: torch.Tensor) -> torch.Tensor: + """AttnPool com P consultas: (t, d) → (d,).""" + scores = seg @ self.consultas.T # (t, P) + pesos = torch.softmax(scores.max(dim=1).values, dim=0) # (t,) + return (pesos.unsqueeze(1) * seg).sum(0) + + # ---------------- consulta (doc 13 §2) ---------------- + @torch.no_grad() + def consulta(self, q: torch.Tensor, k: int = K_RECALL) -> torch.Tensor: + """q: (d,) → top-k resumos (k, d) pela busca em dois estágios.""" + if self.n_segmentos == 0: + return torch.zeros(0, self.d) + self.consultas_totais += 1 + # v5 — correção (bug v4): a grade é treinada sobre resumos BRUTOS + # (insere → treinar_lote(z)); a consulta coarse/fina usa o MESMO + # espaço (q bruto). O proj_retrieval aleatório misturava espaços e + # reduzia o recall a aleatório (sim ≈ 0). Teorema 13.1 recuperado. + q_proj = q + bmu = self.grade.bmu(q_proj.unsqueeze(0))[0].item() + # vizinhança r na grade (grade quadrada lado² → coordenadas) + lado = int(round(self.grade.k ** 0.5)) + ci, cj = bmu // lado, bmu % lado + celulas = [] + for di in range(-VIZ_R, VIZ_R + 1): + for dj in range(-VIZ_R, VIZ_R + 1): + ni, nj = ci + di, cj + dj + if 0 <= ni < lado and 0 <= nj < lado: + celulas.append(ni * lado + nj) + # índice invertido: segmentos cujo BMU está nas células vizinhas + protos = self.grade.w[celulas] # (C, d) + dist = torch.cdist(self.z[:self.m_max], protos) # (M, C) + bmu_dos_segs = dist.argmin(dim=1) # (M,) + mascara = torch.isin(bmu_dos_segs, torch.tensor(celulas)) \ + & self.ocupado + if mascara.any(): + self.hits_celula += 1 + ids = mascara.nonzero(as_tuple=True)[0] + # v5 — fallback global se a vizinhança rende MENOS que k candidatos + # (garante k respostas quando n_segmentos ≥ k — contrato de recall) + if ids.numel() < min(k, int(self.ocupado.sum())): + ids = self.ocupado.nonzero(as_tuple=True)[0] + sims = (self.z[ids] @ q_proj) / (self.z[ids].norm(dim=1) + 1e-6) + top = sims.topk(min(k, ids.numel())).indices + escolhidos = ids[top] + self.utilidade[escolhidos] += 1.0 # LRU utilidade + return self.z[escolhidos] + + def telemetria(self) -> dict: + lado = int(round(self.grade.k ** 0.5)) + return { + "n_segmentos": self.n_segmentos, + "m_max": self.m_max, + "grade": f"{lado}x{lado}", + "hit_celula": (self.hits_celula / max(1, self.consultas_totais)), + "consultas": self.consultas_totais, + } diff --git a/src/khtst/memoria/orquestrador.py b/src/khtst/memoria/orquestrador.py new file mode 100644 index 0000000000000000000000000000000000000000..d809266ea0fbfebe76c1eb65c4a4edc0aba6a6bc --- /dev/null +++ b/src/khtst/memoria/orquestrador.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +"""Orquestrador SOM-of-SOMs: mantém as 8 variantes e roteia por perfil da +tarefa (doc 03 §3, doc 05 §3). Perfil inclui: temporalidade, supervisão +disponível, orçamento de RAM. Cada rota registra telemetria (EQ). + +Uso como memória do modelo: `mapear(emb)` devolve o protótipo da variante +escolhida — memória associativa compacta (RAM: só a variante ativa por tarefa). +""" +from __future__ import annotations + +import torch + +from khtst.memoria.som_base import SOMKohonen +from khtst.memoria.variantes_crescimento import GSOM, GrowingCellStructures, GrowingGrid +from khtst.memoria.variantes_especiais import (CounterpropagationNetwork, + HierarchicalSOM, RecurrentSOM, + SupervisedSOM, TemporalKohonenMap) + + +class OrquestradorSOM: + """Escolha da variante por regra de decisão fundamentada (docs 02–06): + + sequencial + tendência → RSOM (filtro passa-baixa, Teorema 5.4) + sequencial + duração → TKM (janela ponderada, Teorema 5.1) + hierarquia natural / RAM → H-SOM (Teorema 4.1) + dados densos estáveis → Growing Grid (Teorema 2.1) + redundância detectável → GCS (poda por utilidade, Teorema 2.3) + orçamento por granularidade→ GSOM (SF, Teorema 3.1) + roteamento estado→ação → CPN (Teorema 6.1) + rótulos disponíveis → S-SOM (eq. 6.2) + estático simples → SOM base + """ + + def __init__(self, dim: int, cfg, hub=None, semente: int = 2026, + cfg_atencao=None): + self.dim = dim + self.hub = hub + self.variantes = { + "som": SOMKohonen(cfg.ssom["k"], dim, semente=semente), + "gg": GrowingGrid(dim, lado_max=cfg.gg["lado"], + lambda_insercao=cfg.gg["lambda_insercao"], semente=semente), + "gcs": GrowingCellStructures(dim, kmax=cfg.gcs["kmax"], + lambda_insercao=cfg.gcs["lambda_insercao"], semente=semente), + "gsom": GSOM(dim, sf=cfg.gsom["sf"], max_unidades=cfg.gsom["max_unidades"], semente=semente), + "hsom": HierarchicalSOM(dim, k_nivel1=cfg.hsom["k_nivel1"], + k_filho=cfg.hsom["k_filho"], semente=semente), + "tkm": TemporalKohonenMap(dim, k=cfg.tkm["k"], lambda_janela=cfg.tkm["lambda_janela"], semente=semente), + "rsom": RecurrentSOM(dim, k=cfg.rsom["k"], alpha=cfg.rsom["alpha"], semente=semente), + "cpn": CounterpropagationNetwork(dim, dim, k=cfg.cpn["k"], eta0=cfg.cpn["eta0"]), + "ssom": SupervisedSOM(dim, k=cfg.ssom["k"], n_classes=cfg.ssom["n_classes"], semente=semente), + } + self.ativa = "som" + self.motivo = "padrão" + # v5 (doc 16 §§3–4): recall atencional sobre TODAS as variantes + + # invariante ZERO neurônios isolados (Teorema 16.5) + from khtst.memoria.atencao_som import AtencaoEntreVariantes + cfg_atn = dict(cfg_atencao or {}) + if not cfg_atn.get("ativo", True): + self.atencao = None + else: + self.atencao = AtencaoEntreVariantes( + tau=float(cfg_atn.get("tau", 0.5)), + gamma_novidade=float(cfg_atn.get("gamma_novidade", 0.15)), + hub=hub) + + # ---------------- roteamento ---------------- + def escolher(self, perfil: dict) -> str: + """perfil: {'temporal': bool, 'tendencia': bool, 'supervisao': bool, + 'hierarquico': bool, 'ram_apertado': bool, 'rotulo_tarefa': str}""" + if perfil.get("rotulo_tarefa") == "instrucao": + escolha, motivo = "cpn", "roteamento estado→ação (CPN, Teorema 6.1)" + elif perfil.get("supervisao"): + escolha, motivo = "ssom", "rótulos disponíveis (S-SOM, eq. 6.2)" + elif perfil.get("temporal") and perfil.get("tendencia"): + escolha, motivo = "rsom", "tendência do resíduo (Teorema 5.4)" + elif perfil.get("temporal"): + escolha, motivo = "tkm", "duração de padrões (Teorema 5.1)" + elif perfil.get("hierarquico") or perfil.get("ram_apertado"): + escolha, motivo = "hsom", "decomposição hierárquica (Teorema 4.1)" + elif perfil.get("granularidade"): + escolha, motivo = "gsom", "controle por SF (Teorema 3.1)" + elif perfil.get("redundancia"): + escolha, motivo = "gcs", "poda por utilidade (Teorema 2.3)" + elif perfil.get("denso"): + escolha, motivo = "gg", "refinamento de Voronoi (Teorema 2.1)" + else: + escolha, motivo = "som", "estático simples" + self.ativa, self.motivo = escolha, motivo + if self.hub is not None: + self.hub.atributo("som/variante_ativa", hash(escolha) % 1000) + return escolha + + def variante(self): + return self.variantes[self.ativa] + + # ---------------- interface de memória ---------------- + def treinar_memoria(self, dados: torch.Tensor, epocas: int = 2) -> float: + v = self.variante() + if isinstance(v, CounterpropagationNetwork): + v.ajustar((dados, dados), epocas=epocas) # CPN p/ memória: identidade + eq = v.eq(dados, dados) + elif isinstance(v, SupervisedSOM): + v.ajustar(dados, torch.zeros(len(dados)), epocas=epocas) + eq = v.eq(dados) + elif hasattr(v, "ajustar"): + v.ajustar(dados, epocas=epocas) + eq = v.eq(dados) if hasattr(v, "eq") else float("nan") + else: + eq = float("nan") + if self.hub is not None and eq == eq: # não-NaN + self.hub.atributo(f"som/eq_{self.ativa}", eq) + return eq + + def mapear(self, x: torch.Tensor) -> torch.Tensor: + """Memória associativa: protótipos das amostras na variante ativa.""" + v = self.variante() + if isinstance(v, (CounterpropagationNetwork, SupervisedSOM)): + return v.kohonen.w[v.kohonen.bmu(x)[0]] if isinstance(v, CounterpropagationNetwork) \ + else v.w[v.bmu(x)[0]] + if isinstance(v, HierarchicalSOM): + return v.n1.w[v.n1.bmu(x)[0]] + return v.w[v.bmu(x)[0]] + + def mapear_atencao(self, x: torch.Tensor) -> torch.Tensor: + """v5 — recall atencional sobre TODAS as variantes (eq. 16.3–16.4): + ẑ = Σ_v a_v z_v ∈ casco{∪_v W_v} (Teorema 16.3); bônus de novidade + direciona rotas a variantes com neurônios órfãos (Teorema 16.5). + Fallback: roteamento duro (mapear) quando a atenção está desativada.""" + if self.atencao is None: + return self.mapear(x) + return self.atencao.mapear(x, self) + + def estatisticas(self) -> dict: + out = {"ativa": self.ativa, "motivo": self.motivo} + for nome, v in self.variantes.items(): + if hasattr(v, "estatisticas"): + out[nome] = v.estatisticas() + elif hasattr(v, "k"): + out[nome] = {"k": v.k if isinstance(v.k, int) else int(v.k)} + return out + + # ---------------- v4: fase B (doc 15 §2) ---------------- + def taxa_ativos_global(self) -> float: + """A = fração média de neurônios ativos sobre AS 8 VARIANTES (eq. 15.1). + H-SOM delega à camada `n1` (nível 1); GCS usa usos>0 (reseeding).""" + vals = [] + for v in self.variantes.values(): + alvo = getattr(v, "kohonen", None) or getattr(v, "n1", v) + if hasattr(alvo, "taxa_ativos"): + vals.append(alvo.taxa_ativos()) + return sum(vals) / len(vals) if vals else 1.0 + + def eq_medio(self, x: torch.Tensor) -> float: + """EQ médio sobre as variantes avaliáveis (Teorema 15.2 — proxy do + EQ incremental na fase B).""" + vals = [] + for v in self.variantes.values(): + try: + if hasattr(v, "eq"): + vals.append(float(v.eq(x))) + elif hasattr(v, "kohonen"): + vals.append(float(v.kohonen.eq(x))) + except Exception: + continue + return sum(vals) / len(vals) if vals else 0.0 + + def telemetria_ativos(self) -> dict: + """Neurônios ativos POR VARIANTE (métrica obrigatória doc 15 §3.1) — + as 8 variantes, delegando à camada interna quando hierárquica.""" + out = {} + for nome, v in self.variantes.items(): + alvo = getattr(v, "kohonen", None) or getattr(v, "n1", v) + if hasattr(alvo, "taxa_ativos"): + out[f"ativos/{nome}"] = round(float(alvo.taxa_ativos()), 4) + k = getattr(alvo, "k", None) + if k is None: + k = alvo.w.shape[0] if hasattr(alvo, "w") else 0 + out[f"k/{nome}"] = int(k) + out["A_global"] = round(self.taxa_ativos_global(), 4) + return out diff --git a/src/khtst/memoria/som_base.py b/src/khtst/memoria/som_base.py new file mode 100644 index 0000000000000000000000000000000000000000..f8cd692826eb8e693886aee19d3501e15ae80d6f --- /dev/null +++ b/src/khtst/memoria/som_base.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +"""SOM de Kohonen clássico (doc 01) — vetorizado em torch, duas fases: +ordenamento (σ alto) e convergência (σ→σ_f, α tipo Robbins–Monro). + +v2 (doc 10 §2.3 — fase SOM com MÁXIMO de neurônios ativos): +• vencedor com penalidade de novidade RESTRITA A EMPATES (HAKO-v3, Teorema + adaptado): dentro do conjunto quase-empatado C(x) = {j: d²_j ≤ d*²(1+γ)} + o vencedor é argmin d² + λ·u_j·log1p(h_j) — excesso de QE ≤ √(1+γ)−1; +• reseeding de neurônios mortos (h_j EMA < ε_h por janela) com amostras reais + (Teorema 10.7: taxa de ativação → máxima sob cobertura); +• métrica taxa_ativos. + +Métricas: EQ (erro de quantização), TE (erro topográfico), rank efetivo do uso. +RAM: pesos (K×d) + erros acumulados (K) — mínimo possível. +""" +from __future__ import annotations + +import torch + + +class SOMKohonen: + def __init__(self, k: int, dim: int, sigma0: float = 3.0, sigma_f: float = 0.4, + alpha0: float = 0.5, grade: tuple | None = None, semente: int = 2026, + gamma_empate: float = 0.15, eps_ativo: float = 0.02): + self.k, self.dim = k, dim + self.sigma0, self.sigma_f, self.alpha0 = sigma0, sigma_f, alpha0 + self.gamma_empate = gamma_empate # γ do conjunto quase-empatado (Prop. 10.6) + self.eps_ativo = eps_ativo # limiar de neurônio ativo (h EMA) + g = torch.Generator().manual_seed(semente) + self.w = torch.randn(k, dim, generator=g) * 0.1 + # posições na grade (para vizinhança): grade quadrada ou linear + lado = grade or (int(k ** 0.5),) + lado1 = lado[0] + lado2 = max(1, k // lado1) + r = [(i % lado2, i // lado2) for i in range(k)] + self.pos = torch.tensor(r, dtype=torch.float32) # (K, 2) + self.passo = 0 + self.hit_ema = torch.zeros(k) # EMA de acertos h_j + self.erro_ema = torch.zeros(k) # EMA da escala de erro u_j + self.uso_total = torch.zeros(k, dtype=torch.long) + self.n_mortos_resemeados = 0 + + # ---------------- infraestrutura ---------------- + def _vizinhanca(self, c: int, sigma: float) -> torch.Tensor: + d2 = torch.cdist(self.pos[c:c + 1], self.pos).pow(2).squeeze(0) + return torch.exp(-d2 / (2 * sigma ** 2 + 1e-12)) # h_cj (eq. 1.2) + + def _sincronizar_tamanho(self): + """Variantes de crescimento (GG/GSOM) inserem neurônios: EMAs de ativação + crescem junto (novos nascem 'ativos fracos' p/ não serem resemeados).""" + if self.hit_ema.shape[0] < self.k: + falta = self.k - self.hit_ema.shape[0] + self.hit_ema = torch.cat([self.hit_ema, torch.full((falta,), 0.05)]) + self.erro_ema = torch.cat([self.erro_ema, torch.zeros(falta)]) + self.uso_total = torch.cat([self.uso_total, torch.zeros(falta, dtype=torch.long)]) + + def bmu(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """x: (N, d) → (índices c, distâncias²) por amostra (vetorizado).""" + self._sincronizar_tamanho() + d2 = torch.cdist(x, self.w).pow(2) # (N, K) + minimo, c = d2.min(dim=1) + return c, minimo + + def bmu_novidade(self, x: torch.Tensor, lambda_nov: float = 0.25): + """Vencedor com penalidade de novidade RESTRITA A EMPATES (doc 10 §2.3a). + Devolve (c, d²). Fora de C(x), a ordem é a do BMU clássico — garantia + de QE (excesso ≤ √(1+γ)−1).""" + self._sincronizar_tamanho() + d2 = torch.cdist(x, self.w).pow(2) # (N, K) + d2_min, _ = d2.min(dim=1, keepdim=True) + dentro = d2 <= d2_min * (1.0 + self.gamma_empate) # (N, K) conjunto C + penal = lambda_nov * self.erro_ema.clamp(min=0) * torch.log1p(self.hit_ema) + score = d2 + penal.unsqueeze(0) + score = torch.where(dentro, score, torch.inf) + c_alt = score.argmin(dim=1) + inf_total = torch.isinf(score).all(dim=1) + c = torch.where(inf_total, d2.argmin(dim=1), c_alt) + return c, d2.gather(1, c.unsqueeze(1)).squeeze(1) + + # ---------------- treino ---------------- + def treinar_lote(self, x: torch.Tensor) -> float: + """Regra de Kohonen amostra a amostra (eq. 1.1, Teorema 0.1), vetorizada + sobre as K unidades, com vencedor de novidade restrita a empates e + reseeding de mortos. Retorna o EQ inicial do lote (métrica).""" + d2 = torch.cdist(x, self.w).pow(2) # (N, K) + eq = float(d2.min(dim=1).values.mean()) + self._sincronizar_tamanho() + c, d2_win = self.bmu_novidade(x) # vencedores (com novidade) + mortos = (self.hit_ema < self.eps_ativo).nonzero(as_tuple=True)[0] + for i in range(x.shape[0]): + self.n_amostras = getattr(self, "n_amostras", 0) + 1 + frac = min(1.0, self.n_amostras / max(self.total_planejado, 1)) + sigma = self.sigma0 * (self.sigma_f / self.sigma0) ** frac + alpha = self.alpha0 / (1.0 + 0.01 * self.n_amostras) # Robbins–Monro (R1) + ci = int(c[i]) + h = self._vizinhanca(ci, sigma) # (K,) + self.w += alpha * h.unsqueeze(1) * (x[i].unsqueeze(0) - self.w) + self.uso_total[ci] += 1 + # telemetria de ativação POR LOTE (janela estável — não por amostra): + # um único acerto no lote mantém o neurônio ativo na janela seguinte + with torch.no_grad(): + hits = torch.bincount(c, minlength=self.k) + self.hit_ema = 0.92 * self.hit_ema + 0.08 * (hits > 0).float() + erro_med = torch.zeros(self.k) + erro_med.scatter_add_(0, c, d2_win.clamp(min=0)) + n_hit = hits.clamp(min=1).float() + self.erro_ema = 0.9 * self.erro_ema + 0.1 * (erro_med / n_hit) + # reseeding: mortos recebem amostras reais (Teorema 10.7) + with torch.no_grad(): + if mortos.numel() and x.shape[0] > 0: + fontes = x[torch.randint(0, x.shape[0], (int(mortos.numel()),))] + self.w[mortos] = fontes + self.hit_ema[mortos] = 0.08 # recém-nascido: conta ativo + self.erro_ema[mortos] = 0.0 + self.n_mortos_resemeados += int(mortos.numel()) + self.passo += 1 + return eq + + def ajustar(self, dados: torch.Tensor, epocas: int = 3, total_planejado: int | None = None): + self.total_planejado = total_planejado or (epocas * len(dados)) + historico = [] + for _ in range(epocas): + perm = torch.randperm(len(dados)) + for i in range(0, len(dados), 64): + historico.append(self.treinar_lote(dados[perm[i:i + 64]])) + return historico + + # ---------------- métricas (doc 01 §5) ---------------- + def eq(self, x: torch.Tensor) -> float: + return float(self.bmu(x)[1].mean()) + + def te(self, x: torch.Tensor) -> float: + d2 = torch.cdist(x, self.w).pow(2) + dois = d2.topk(2, dim=1, largest=False).indices + pos = self.pos + dist = (pos[dois[:, 0]] - pos[dois[:, 1]]).pow(2).sum(dim=1) + return float((dist > 1.5).float().mean()) + + def uso(self, x: torch.Tensor) -> torch.Tensor: + """Histograma de uso das unidades (para rank efetivo na telemetria).""" + c, _ = self.bmu(x) + return torch.bincount(c, minlength=self.k).float() + + # ---------------- fase SOM v2: máximo de neurônios ativos ---------------- + def taxa_ativos(self) -> float: + """Fração de neurônios ativos (h EMA ≥ ε) — doc 10 §2.4.""" + return float((self.hit_ema >= self.eps_ativo).float().mean()) + + def estatisticas(self) -> dict: + return {"k": self.k, "taxa_ativos": self.taxa_ativos(), + "resemeados": self.n_mortos_resemeados, + "usos": int(self.uso_total.sum())} diff --git a/src/khtst/memoria/variantes_crescimento.py b/src/khtst/memoria/variantes_crescimento.py new file mode 100644 index 0000000000000000000000000000000000000000..1dfb575081f745bd7e2b10b90c9b6a2ecefc93a4 --- /dev/null +++ b/src/khtst/memoria/variantes_crescimento.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +"""Variantes de crescimento (docs 02–03): Growing Grid, Growing Cell Structures +e GSOM. Inserções só onde o erro local justifica (Teoremas 2.1/3.1) — crescer +nunca aumenta E_q; GCS pode podar por utilidade (Teorema 2.3). + +Diferencial de implementação: todas operam sobre amostras já embeddings +(d≈192) e expõem `eq()` e `estatisticas()` para a telemetria. +""" +from __future__ import annotations + +import torch + +from khtst.memoria.som_base import SOMKohonen + + +class GrowingGrid(SOMKohonen): + """GG (Fritzke 1995): começa 2×2; a cada λ amostras insere linha/coluna na + unidade de maior erro acumulado, até L_max².""" + + def __init__(self, dim: int, lado_max: int = 8, lambda_insercao: int = 400, + beta: float = 0.9995, semente: int = 2026): + super().__init__(4, dim, semente=semente) + self.lado, self.lado_max = 2, lado_max + self.lambda_, self.beta = lambda_insercao, beta + self.erro = torch.zeros(self.k) + self.n_desde_insercao = 0 + + def treinar_lote(self, x: torch.Tensor) -> float: + eq = super().treinar_lote(x) + c, d2 = self.bmu(x) + self.erro.index_add_(0, c, d2) + self.erro *= self.beta + self.n_desde_insercao += len(x) + if self.n_desde_insercao >= self.lambda_: + self.n_desde_insercao = 0 + self._tentar_crescer(x) + return eq + + def _tentar_crescer(self, x: torch.Tensor) -> None: + if self.lado >= self.lado_max or self.k >= self.lado_max ** 2: + return + q = int(self.erro.argmax()) + # insere um clone vizinho de q (linha/coluna simplificada: novo protótipo + # entre q e seu vizinho mais distante — Teorema 2.1) + dists = torch.cdist(self.pos[q:q + 1], self.pos).squeeze(0) + dists[q] = -1 + v = int(dists.argmax()) + w_novo = (self.w[q] + self.w[v]) / 2 + self.w = torch.cat([self.w, w_novo.unsqueeze(0)]) + self.pos = torch.cat([self.pos, ((self.pos[q] + self.pos[v]) / 2).unsqueeze(0)]) + self.erro = torch.cat([self.erro, self.erro[q:q + 1] / 2]) + self.erro[q] /= 2 + self.k += 1 + self.lado = max(self.lado, int(self.k ** 0.5) + (1 if int(self.k ** 0.5) ** 2 < self.k else 0)) + + +class GrowingCellStructures: + """GCS (Fritzke 1994): topologia simplicial aproximada por grafo de + vizinhança; inserção na aresta (q, f) e poda por utilidade mínima.""" + + def __init__(self, dim: int, kmax: int = 48, lambda_insercao: int = 300, + beta: float = 0.9995, semente: int = 2026): + g = torch.Generator().manual_seed(semente) + self.w = torch.randn(3, dim, generator=g) * 0.1 # triângulo inicial + self.erro = torch.zeros(3) + self.usos = torch.zeros(3) + self.kmax, self.lambda_, self.beta = kmax, lambda_insercao, beta + self.vizinhos = {(0, 1), (1, 2), (2, 0)} + self.passo = 0 + + @torch.no_grad() + def treinar_lote(self, x: torch.Tensor) -> float: + d2 = torch.cdist(x, self.w).pow(2) + c = d2.argmin(dim=1) + eq = float(d2.gather(1, c.unsqueeze(1)).mean()) + for i, ci in enumerate(c.tolist()): + self.w[ci] += 0.05 * (x[i] - self.w[ci]) + self.erro[ci] += d2[i, ci] + self.usos[ci] += 1 + self.erro *= self.beta + self.passo += len(x) + if self.passo >= self.lambda_: + self.passo = 0 + if self.w.shape[0] < self.kmax: + self._inserir() + else: + self._podar() + return eq + + def _inserir(self): + q = int(self.erro.argmax()) + dists = torch.cdist(self.w[q:q + 1], self.w).squeeze(0) + dists[q] = -1 + f = int(dists.argmax()) + w_novo = (self.w[q] + self.w[f]) / 2 + self.w = torch.cat([self.w, w_novo.unsqueeze(0)]) + novo = self.w.shape[0] - 1 + self.erro = torch.cat([self.erro, self.erro[q:q + 1] / 2]) + self.usos = torch.cat([self.usos, torch.zeros(1)]) + self.erro[q] /= 2 + self.vizinhos.discard((min(q, f), max(q, f))) + for v in (q, f): + self.vizinhos.add((min(novo, v), max(novo, v))) # conectividade: Teorema 2.2 + + def _podar(self): + utilidade = self.usos / (self.usos.max() + 1e-12) + i = int(utilidade.argmin()) + if utilidade[i] > 0.05 or self.w.shape[0] <= 3: + return + keep = [j for j in range(self.w.shape[0]) if j != i] + self.w = self.w[keep] + self.erro = self.erro[keep] + self.usos = self.usos[keep] + self.vizinhos = {(a if a < i else a - 1, b if b < i else b - 1) + for a, b in self.vizinhos if a != i and b != i} + + @torch.no_grad() + def ajustar(self, dados: torch.Tensor, epocas: int = 2): + hist = [] + for _ in range(epocas): + perm = torch.randperm(len(dados)) + for i in range(0, len(dados), 64): + hist.append(self.treinar_lote(dados[perm[i:i + 64]])) + # v4 — SEM NEURÔNIOS ISOLADOS (Teorema 15.2): células sem uso em + # janela longa recebem amostras reais (consistente com as demais) + mortas = (self.usos == 0).nonzero(as_tuple=True)[0] + if mortas.numel() and len(dados) > 0: + fontes = dados[torch.randperm(len(dados))[:mortas.numel()]] + self.w[mortas] = fontes + self.usos[mortas] = 1.0 + self.erro[mortas] = 0.0 + return hist + + def taxa_ativos(self) -> float: + """v4: fração de células com uso > 0 (GCS já poda inúteis — + Teorema 2.3 —, mas o reseeding garante 100% mesmo sem poda).""" + return float((self.usos > 0).float().mean()) + + def eq(self, x: torch.Tensor) -> float: + return float(torch.cdist(x, self.w).pow(2).min(dim=1).values.mean()) + + def estatisticas(self) -> dict: + return {"k": int(self.w.shape[0]), "arestas": len(self.vizinhos), + "eq_treino": float(self.erro.mean())} + + +class GSOM(SOMKohonen): + """GSOM (Alahakoon 2000): GT = -D·ln(SF) controla crescimento de fronteiras + (Teorema 3.1). Implementação: grade retangular com posições inteiras; nova + unidade espelhada na direção livre.""" + + def __init__(self, dim: int, sf: float = 0.35, lambda_vizinhos: float = 0.3, + max_unidades: int = 96, semente: int = 2026): + super().__init__(4, dim, semente=semente) + self.gt = -dim * 0.0 - torch.log(torch.tensor(sf)).item() * 1.0 # escala ajustada + self.gt = float(-torch.log(torch.tensor(sf))) + self.lambda_viz = lambda_vizinhos + self.max_unidades = max_unidades + self.erro = torch.zeros(4) + self.ocupadas = {(0, 0), (0, 1), (1, 0), (1, 1)} + + @torch.no_grad() + def treinar_lote(self, x: torch.Tensor) -> float: + eq = super().treinar_lote(x) + c, d2 = self.bmu(x) + self.erro.index_add_(0, c, d2 * self.lambda_viz) + gatilho = (self.erro > self.gt).nonzero().squeeze(1) + for i in gatilho.tolist(): + self._crescer(i) + self.erro[i] = 0.0 + return eq + + def _crescer(self, i: int): + if self.k >= self.max_unidades: + return + pos = self.pos[i].tolist() + for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)): + nova = (pos[0] + dx, pos[1] + dy) + if not any(abs(p[0] - nova[0]) + abs(p[1] - nova[1]) < 1e-6 for p in self.ocupadas): + self.w = torch.cat([self.w, self.w[i:i + 1] + torch.randn_like(self.w[i:i + 1]) * 0.01]) + self.pos = torch.cat([self.pos, torch.tensor([[nova[0], nova[1]]], dtype=torch.float32)]) + self.erro = torch.cat([self.erro, torch.zeros(1)]) + self.ocupadas.add(nova) + self.k += 1 + return + + def estatisticas(self) -> dict: + return {"k": int(self.k), "gt": round(self.gt, 3), + "erro_medio": float(self.erro.mean())} diff --git a/src/khtst/memoria/variantes_especiais.py b/src/khtst/memoria/variantes_especiais.py new file mode 100644 index 0000000000000000000000000000000000000000..23f8748a0650ca8035b3ecd1a75f1b3c67b1856c --- /dev/null +++ b/src/khtst/memoria/variantes_especiais.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +"""Variantes hierárquicas, temporais e supervisionadas (docs 04–06): + H-SOM — memória multinível (nível 1 sempre em RAM, filhos sob demanda); + TKM — vencedor por erro acumulado na janela (Teorema 5.1); + RSOM — contexto EMA do resíduo (Teoremas 5.2–5.4, cota B verificada); + CPN — Kohonen (WTA) + Grossberg (LMS) = regressão por partes (Teorema 6.1); + S-SOM — perda combinada quantização + CE supervisionada (eq. 6.2). +""" +from __future__ import annotations + +import torch + +from khtst.memoria.som_base import SOMKohonen + + +class HierarchicalSOM: + """Dois níveis (doc 04): nível 1 global; filho por célula treinado só com + as amostras da célula — estágio B não degrada nível 1 (Teorema 4.2).""" + + def __init__(self, dim: int, k_nivel1: int = 12, k_filho: int = 10, semente: int = 2026): + self.n1 = SOMKohonen(k_nivel1, dim, sigma0=2.5, semente=semente) + self.filhos: dict[int, SOMKohonen | None] = {} + self.k_filho, self.dim = k_filho, dim + self.semente = semente + + def ajustar(self, dados: torch.Tensor, epocas: int = 2): + self.n1.ajustar(dados, epocas=min(2, epocas), total_planejado=epocas * len(dados)) + c, _ = self.n1.bmu(dados) + # treina filhos em ordem de massa de erro (top-4 apenas — RAM) + eq1 = torch.zeros(self.n1.k) + for ci in c.tolist(): + pass + d2 = torch.cdist(dados, self.n1.w).pow(2).min(dim=1).values + eq1.index_add_(0, c, d2) + for ci in eq1.topk(min(4, self.n1.k)).indices.tolist(): + masc = (c == ci) + if int(masc.sum()) < self.k_filho: + continue + filho = SOMKohonen(self.k_filho, self.dim, sigma0=1.5, + semente=self.semente + ci) + filho.ajustar(dados[masc], epocas=epocas, total_planejado=epocas * int(masc.sum())) + self.filhos[ci] = filho + + def eq(self, x: torch.Tensor) -> float: + c, _ = self.n1.bmu(x) + d2 = torch.cdist(x, self.n1.w).pow(2).min(dim=1).values + total = d2.clone() + for ci, filho in self.filhos.items(): + masc = (c == ci) + if masc.any(): + total[masc] = filho.eq(x[masc]) * torch.ones(int(masc.sum())) + return float(total.mean()) + + +class TemporalKohonenMap(SOMKohonen): + """TKM: D_i(t) = Σ λ^s ‖x(t−s) − w_i‖² (eq. 5.1). Janela efetiva S_ε logada.""" + + def __init__(self, dim: int, k: int = 32, lambda_janela: float = 0.5, + janela: int = 8, semente: int = 2026): + super().__init__(k, dim, sigma0=2.0, semente=semente) + self.lam = lambda_janela + self.janela = janela + self.historico: list[torch.Tensor] = [] + + def bmu_temporal(self, seq: torch.Tensor) -> int: + """seq: (T, d) — vencedor pela soma ponderada na janela.""" + self.historico = list(seq[-self.janela:]) + pesos = torch.tensor([self.lam ** s for s in range(len(self.historico))][::-1]) + d2 = torch.stack([torch.cdist(self.w, h.unsqueeze(0)).squeeze(1).pow(2) + for h in self.historico]) # (Tj, K) + return int((pesos.unsqueeze(1) * d2).sum(0).argmin()) + + +class RecurrentSOM(SOMKohonen): + """RSOM: c_i ← (1−α)c_i + α(x − w_i) (eq. 5.2); vencedor = min ‖c_i‖. + Cota ‖c‖≤B (Teorema 5.3) verificada por asserção local — violação é BUG + (contratos revisados pelo Agente Engenheiro antes de cada modificação).""" + + def __init__(self, dim: int, k: int = 32, alpha: float = 0.3, semente: int = 2026): + super().__init__(k, dim, sigma0=2.0, semente=semente) + self.alpha = alpha + self.c = torch.zeros(k, dim) + + def passo_temporal(self, x: torch.Tensor) -> tuple[int, str | None]: + """x: (d) amostra corrente. Atualiza contexto e retorna (vencedor, evento). + Teorema 5.3: como α·‖x‖ domina o EMA e ‖x‖ é limitado pela normalização + das entradas, ‖c_i‖ ≤ B = max(1, ‖x‖) — violação é BUG (asserção local).""" + residuo = x.unsqueeze(0) - self.w # (K, d) + self.c = (1 - self.alpha) * self.c + self.alpha * residuo + cota_b = max(1.0, abs(float(x.norm()))) + evento = None + if float(self.c.norm(dim=1).max()) > cota_b * 1.05 + 1e-4: + evento = f"BUG (doc 5.3): cota B do RSOM violada (B={cota_b:.3f})" + raise AssertionError(evento) + ci = int(self.c.norm(dim=1).argmin()) + self.w[ci] += 0.1 * (x - self.w[ci]) + return ci, evento + + def ajustar_sequencias(self, seqs: list[torch.Tensor]): + for seq in seqs: + for t in range(len(seq)): + self.passo_temporal(seq[t]) + + +class CounterpropagationNetwork: + """CPN: camada Kohonen congelada após fase 1; Grossberg converge para a + média condicional por célula (Teorema 6.1) — regressão por partes.""" + + def __init__(self, dim_entrada: int, dim_saida: int, k: int = 24, eta0: float = 0.05): + self.kohonen = SOMKohonen(k, dim_entrada, sigma0=2.5) + self.v = torch.zeros(k, dim_saida) + self.eta0, self.passo = eta0, 0 + + def ajustar(self, pares: tuple[torch.Tensor, torch.Tensor], epocas: int = 2): + x, y = pares + self.kohonen.ajustar(x, epocas=epocas) # fase 1 + for _ in range(epocas): # fase 2 (W congelado) + perm = torch.randperm(len(x)) + for i in perm: + c, _ = self.kohonen.bmu(x[i:i + 1]) + ci = int(c[0]) + self.passo += 1 + eta = self.eta0 / (1.0 + 0.01 * self.passo) # Robbins–Monro + self.v[ci] += eta * (y[i] - self.v[ci]) + + def prever(self, x: torch.Tensor) -> torch.Tensor: + c, _ = self.kohonen.bmu(x) + return self.v[c] # (N, dim_saida) + + def eq(self, x: torch.Tensor, y: torch.Tensor) -> float: + return float((self.prever(x) - y).pow(2).mean()) + + +class SupervisedSOM(SOMKohonen): + """S-SOM: quantização + CE supervisionada (eq. 6.2). λ auto-normalizado + (Teorema 6.2); guarda λ·v_max ≤ 0.1·α (Teorema 6.3).""" + + def __init__(self, dim: int, k: int = 24, n_classes: int = 8, lambda_sup: float = 0.5, + semente: int = 2026): + super().__init__(k, dim, sigma0=2.0, semente=semente) + self.v = torch.zeros(k, n_classes) + self.lambda_sup = lambda_sup + + @torch.no_grad() + def ajustar(self, dados: torch.Tensor, rotulos: torch.Tensor, epocas: int = 2): + """v4 — SEM NEURÔNIOS ISOLADOS (doc 01 §3 + Teorema 15.2): além da + atualização supervisionada, mantém hit_ema (taxa de ativos) e ressemeia + neurônios mortos com amostras reais ao fim de CADA época — o mesmo + contrato das demais variantes (reseeding guloso, nascimento com + hit 0.08 > ε_ativo).""" + for _ in range(epocas): + perm = torch.randperm(len(dados)) + for i in perm: + xi, yi = dados[i], rotulos[i] + c = int(torch.cdist(xi.unsqueeze(0), self.w).squeeze(0).argmin()) + self.w[c] += 0.05 * (xi - self.w[c]) # termo quantização + p = torch.softmax(self.v[c], dim=0) # termo CE (eq. 6.2) + onehot = torch.zeros_like(p) + onehot[int(yi)] = 1.0 + self.v[c] += (self.lambda_sup * 0.1) * (onehot - p) + # hit do vencedor (mesma EMA do treinar_lote — taxa de ativos) + self.hit_ema *= 0.92 + self.hit_ema[c] += 0.08 + # reseeding de mortos com amostras reais (Teorema 15.2 — guloso) + mortos = (self.hit_ema < self.eps_ativo).nonzero(as_tuple=True)[0] + if mortos.numel() and len(dados) > 0: + fontes = dados[torch.randint(0, len(dados), (int(mortos.numel()),))] + self.w[mortos] = fontes + self.hit_ema[mortos] = 0.08 # recém-nascido conta ativo + self.erro_ema[mortos] = 0.0 + self.n_mortos_resemeados += int(mortos.numel()) + + def classificar(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """→ (classes, confianças) — confiança alimenta o limiar PDCA (Teorema 9.5).""" + c = torch.cdist(x, self.w).argmin(dim=1) + p = torch.softmax(self.v[c], dim=1) + confianca, classe = p.max(dim=1) + return classe, confianca diff --git a/src/khtst/metricas/__init__.py b/src/khtst/metricas/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c9c895f85402d92f01343c81cf4acd2c7c346362 --- /dev/null +++ b/src/khtst/metricas/__init__.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +"""Pacote de MÉTRICAS da v6 (doc 18) — requisito do usuário, quatro famílias: + + alinhamento : CLIP Score, Image-Text Matching (ITM), PPL Multimodal + geracao_texto : CIDEr, BLEU 1–4, ROUGE-L, METEOR (PT-BR determinístico) + benchmarks : MMMU (estilo), MME/MM-Bench (estilo), MathVista (estilo) + — proxies PT-BR honestos, fórmulas oficiais + som_metricas : QE, TE, Medida de Distorção, σ, α, Weight Codebook Drift, + Frequência de Ativação, U-Matrix (qualidade/dinâmica/espaço) + +Todos os avaliadores são determinísticos (semente fixa) e CPU-fechados. +""" +from khtst.metricas.alinhamento import (CabecaITM, avaliar_clip, avaliar_itm, + clip_score, codigos_visuais, + embed_imagem, embed_texto, + ppl_multimodal_texto, + ppl_visual_bigrama, treinar_itm) +from khtst.metricas.benchmarks import (avaliar_mathvista, avaliar_mmcq, + avaliar_mme, avaliar_todos, + construir_itens) +from khtst.metricas.geracao_texto import (bleu, cider, meteor, pacote_completo, + rouge_l) +from khtst.metricas.som_metricas import (erro_quantizacao, erro_topologico, + frequencia_ativacao, + medida_distorcao, metricas_variante, + peso_codebook_drift, u_matrix) + +__all__ = [ + # alinhamento + "CabecaITM", "avaliar_clip", "avaliar_itm", "clip_score", "codigos_visuais", + "embed_imagem", "embed_texto", "ppl_multimodal_texto", "ppl_visual_bigrama", + "treinar_itm", + # geração + "bleu", "cider", "meteor", "rouge_l", "pacote_completo", + # benchmarks + "avaliar_mathvista", "avaliar_mmcq", "avaliar_mme", "avaliar_todos", + "construir_itens", + # SOM + "erro_quantizacao", "erro_topologico", "frequencia_ativacao", + "medida_distorcao", "metricas_variante", "peso_codebook_drift", "u_matrix", +] diff --git a/src/khtst/metricas/alinhamento.py b/src/khtst/metricas/alinhamento.py new file mode 100644 index 0000000000000000000000000000000000000000..85f2e5c8564fc82dc17e6e35ef6ee52fda92d87e --- /dev/null +++ b/src/khtst/metricas/alinhamento.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- +"""Métricas de ALINHAMENTO E CROSS-MODALIDADE (doc 18 §3.1): + + CLIP SCORE — CLIPScore(c,v) = 2.5·cos(f_txt(c), f_img(v)) (Hessel et al. 2021). + O espaço alinhado é o da KHTST (fusão treinada com pares reais + caption/VQA) — proxy honesto, plugável a qualquer par de + encoders; controle NEGATIVO (pares embaralhados) acompanha. + ITM — cabeça binária g([t;v]) para Image-Text Matching: pares reais + (y=1) × embaralhados (y=0); acurácia e F1 de validação. + PPL MULTIMODAL — (a) PPL de tokens de TEXTO condicionados a prefixo VISUAL/ + ÁUDIO (a corrente mista que o modelo treinou); (b) PPL de + CÓDIGOS VISUAIS (patches quantizados por codebook VQ) com + bigrama Laplace — previsibilidade da corrente visual. +""" +from __future__ import annotations + +import math +import random + +import torch +import torch.nn as nn + + +# ---------------- CLIP Score ---------------- +@torch.no_grad() +def embed_texto(modelo, tk, texto: str, tarefa: str = "imagem_caption", + max_len: int = 48) -> torch.Tensor: + """f_txt: embedding médio do tronco (d,) — espaço compartilhado do modelo.""" + ids = tk.encode(texto, tarefa=tarefa, max_len=max_len) + t = torch.tensor([ids]) if ids else torch.zeros(1, 1, dtype=torch.long) + ctx = modelo.contexto(t) # (1, d) — já média temporal + if ctx.dim() == 3: + ctx = ctx.mean(dim=1) + return ctx.squeeze(0) + + +@torch.no_grad() +def embed_imagem(modelo, img: torch.Tensor) -> torch.Tensor: + """f_img: embedding [CLS] do encoder ViT (d,) — já agregado.""" + saida = modelo.enc_imagem(img.unsqueeze(0)) # (1, d) + if saida.dim() == 3: + saida = saida.mean(dim=1) + return saida.reshape(-1) + + +def clip_score(v_txt: torch.Tensor, v_img: torch.Tensor) -> float: + """CLIPScore = 2.5·cos — cosseno com normalização L2 explícita.""" + a = v_txt / (v_txt.norm() + 1e-9) + b = v_img / (v_img.norm() + 1e-9) + return float(2.5 * (a * b).sum()) + + +@torch.no_grad() +def avaliar_clip(modelo, tk, pares: list[tuple[str, torch.Tensor]], + n_controle: int | None = 64) -> dict: + """CLIP Score médio em pares REAIS + controle NEGATIVO (texto aleatório de + outro par). Alinhamento real ⇒ clip_real − clip_controle > 0.""" + if not pares: + return {"clip_real": None, "clip_controle": None, "n": 0} + vet_t = [embed_texto(modelo, tk, t) for t, _ in pares] + vet_i = [embed_imagem(modelo, i) for _, i in pares] + real = [clip_score(a, b) for a, b in zip(vet_t, vet_i)] + ctrl = [] + rng = random.Random(2026) + if n_controle: + for _ in range(min(n_controle, len(pares))): + j = rng.randrange(len(pares)) + k = rng.randrange(len(pares)) + if j == k: + k = (k + 1) % len(pares) + ctrl.append(clip_score(vet_t[j], vet_i[k])) + return {"clip_real": round(sum(real) / len(real), 4), + "clip_controle": round(sum(ctrl) / len(ctrl), 4) if ctrl else None, + "margem": round(sum(real) / len(real) - (sum(ctrl) / len(ctrl) if ctrl else 0.0), 4), + "n": len(pares)} + + +# ---------------- ITM ---------------- +class CabecaITM(nn.Module): + """Cabeça binária Image-Text Matching: g([t̂; v̂; t̂⊙v̂]) → logit.""" + + def __init__(self, d: int, oculto: int = 64): + super().__init__() + self.net = nn.Sequential( + nn.Linear(d * 3, oculto), nn.Tanh(), + nn.Linear(oculto, 1)) + + def forward(self, t: torch.Tensor, v: torch.Tensor) -> torch.Tensor: + z = torch.cat([t, v, t * v], dim=-1) + return self.net(z).squeeze(-1) + + +def treinar_itm(modelo, tk, pares: list[tuple[str, torch.Tensor]], + epocas: int = 3, lote: int = 16, lr: float = 1e-3, + semente: int = 2026) -> tuple[CabecaITM, dict]: + """Treina a cabeça ITM sobre o modelo CONGELADO (pares reais y=1, + embaralhados y=0). Retorna (cabeça, histórico).""" + if len(pares) < 4: + return CabecaITM(modelo.d), {"erro": "pares insuficientes"} + rng = random.Random(semente) + vt, vi = [], [] + for t, img in pares: + vt.append(embed_texto(modelo, tk, t)) + vi.append(embed_imagem(modelo, img)) + n = len(vt) + cabeca = CabecaITM(modelo.d) + opt = torch.optim.Adam(cabeca.parameters(), lr=lr) + perda_fn = nn.BCEWithLogitsLoss() + hist = [] + for ep in range(epocas): + idx = list(range(n)) + rng.shuffle(idx) + total, nep = 0.0, 0 + for i0 in range(0, n - 1, lote // 2): # metade real + metade shuf + pedaco = idx[i0:i0 + lote // 2] + if not pedaco: + continue + t_reais = torch.stack([vt[i] for i in pedaco]) + v_reais = torch.stack([vi[i] for i in pedaco]) + j_outros = [rng.choice([k for k in range(n) if k != i]) + for i in pedaco] + t_falsos = torch.stack([vt[j] for j in j_outros]) + y = torch.tensor([1.0] * len(pedaco) + [0.0] * len(pedaco)) + zt = torch.cat([t_reais, t_falsos], 0) + zv = torch.cat([v_reais, v_reais], 0) + logit = cabeca(zt, zv) + perda = perda_fn(logit, y) + opt.zero_grad() + perda.backward() + opt.step() + total += float(perda) + nep += 1 + hist.append(total / max(nep, 1)) + return cabeca, {"perda_final": round(hist[-1], 6) if hist else None, + "pares_treino": 2 * n} + + +def avaliar_itm(cabeca: CabecaITM, modelo, tk, + pares: list[tuple[str, torch.Tensor]], semente: int = 77) -> dict: + """Acurácia + F1 da ITM em pares de VALIDAÇÃO (reais × embaralhados).""" + if len(pares) < 4: + return {"acuracia": None, "f1": None} + rng = random.Random(semente) + ys, ps = [], [] + with torch.no_grad(): + for i, (t, img) in enumerate(pares): + vt = embed_texto(modelo, tk, t) + vi = embed_imagem(modelo, img) + ys.append(1.0) + ps.append(float(cabeca(vt.unsqueeze(0), vi.unsqueeze(0)))) + j = rng.choice([k for k in range(len(pares)) if k != i]) + vt2 = embed_texto(modelo, tk, pares[j][0]) + ys.append(0.0) + ps.append(float(cabeca(vt2.unsqueeze(0), vi.unsqueeze(0)))) + prev = [1.0 if p > 0 else 0.0 for p in ps] + acc = sum(float(a == b) for a, b in zip(prev, ys)) / len(ys) + tp = sum(1 for a, b in zip(prev, ys) if a == 1 and b == 1) + fp = sum(1 for a, b in zip(prev, ys) if a == 1 and b == 0) + fn = sum(1 for a, b in zip(prev, ys) if a == 0 and b == 1) + f1 = 2 * tp / max(2 * tp + fp + fn, 1) + return {"acuracia": round(acc, 4), "f1": round(f1, 4), + "n": len(ys), "tp": tp, "fp": fp, "fn": fn} + + +# ---------------- PPL Multimodal ---------------- +@torch.no_grad() +def ppl_multimodal_texto(modelo, tk, registros, n_max: int = 24) -> dict: + """PPL de tokens de TEXTO condicionados a PREFIXO multimodal real + (imagem/áudio) — a corrente mista treinada. Menor = melhor.""" + from khtst.treino.treinador import TAREFAS_AUDIO, TAREFAS_IMAGEM, \ + _audio_para_tensor, _imagem_para_tensor + pool = [r for r in registros + if r["tarefa"] in TAREFAS_IMAGEM or r["tarefa"] in TAREFAS_AUDIO] + pool = pool[:n_max] + if not pool: + return {"ppl_mm": None, "n": 0} + ce_total, n_tok = 0.0, 0 + L = modelo.cfg.modelo.comprimento_ctx + for r in pool: + tarefa = r["tarefa"] + inp = tk.encode(r.get("entrada") or "", tarefa=tarefa, max_len=L // 2) + saida_fonte = r.get("saida") or r.get("texto") or "" + out = tk.encode(saida_fonte, tarefa=None, + max_len=max(L - len(inp) - 4, 8), com_bos=False) + if len(out) < 4: + continue + seq = inp + out + ids = torch.zeros(1, len(seq), dtype=torch.long) + ids[0] = torch.tensor(seq) + alvo = torch.full((1, len(seq)), -100, dtype=torch.long) + ninp = len(inp) + alvo[0, ninp:len(seq)] = ids[0, ninp:len(seq)] + entradas = {} + lado = modelo.cfg.modelo.imagem["resolucao"] + if tarefa in TAREFAS_IMAGEM and r.get("imagem"): + entradas["imagem"] = _imagem_para_tensor(r["imagem"], lado).unsqueeze(0) + if tarefa in TAREFAS_AUDIO and r.get("audio"): + onda = _audio_para_tensor(r["audio"]) + entradas["audio"] = onda.unsqueeze(0) + emb_prefixo = modelo.codificar_multimodal(entradas) if entradas else None + _, perda = modelo(ids, alvo=alvo, emb_prefixo=emb_prefixo, tarefa=tarefa) + n_alvo = int((alvo != -100).sum()) + ce_total += float(perda) * n_alvo + n_tok += n_alvo + if n_tok == 0: + return {"ppl_mm": None, "n": 0} + return {"ppl_mm": round(math.exp(min(ce_total / n_tok, 12)), 2), + "ce_mm": round(ce_total / n_tok, 4), "n": n_tok} + + +def codigos_visuais(modelo, img: torch.Tensor, n_codigos: int = 64, + codebook: torch.Tensor | None = None, + grade: int = 3) -> list[int]: + """Corrente de CÓDIGOS VISUAIS: divide a imagem numa grade grade×grade de + crops, encoda cada crop pelo ViT e quantiza num CODEBOOK ADAPTADO por + kmeans-lite (seme fixa) — sequência real (grade² códigos) p/ bigrama.""" + C, H, W = img.shape + gh, gw = H // grade, W // grade + embs = [] + with torch.no_grad(): + for i in range(grade): + for j in range(grade): + crop = img[:, i * gh:(i + 1) * gh, j * gw:(j + 1) * gw] + if crop.shape[1] < 8 or crop.shape[2] < 8: + continue + saida = modelo.enc_imagem(crop.unsqueeze(0)) + if saida.dim() == 3: + saida = saida.mean(dim=1) + embs.append(saida.reshape(-1)) + if not embs: + return [] + Z = torch.stack(embs) # (G², d) + # kmeans-lite determinístico: codebook adaptado aos embeddings reais + g = torch.Generator().manual_seed(2026) + k = min(n_codigos, max(2, Z.shape[0])) + centros = Z[torch.randperm(Z.shape[0], generator=g)[:k]].clone() + for _ in range(10): + atrib = torch.cdist(Z, centros).argmin(dim=1) + for c in range(k): + masc = atrib == c + if masc.any(): + centros[c] = Z[masc].mean(dim=0) + atrib = torch.cdist(Z, centros).argmin(dim=1) + return [int(a) for a in atrib.tolist()] + + +def ppl_visual_bigrama(codigos_por_imagem: list[list[int]], + n_codigos: int = 64, alfa: float = 0.5) -> dict: + """PPL da corrente VISUAL: bigrama com suavização de Laplace-α. + p(c_t|c_{t−1}) = (cont + α)/(cont_uni + α·K).""" + cont_bi: dict[tuple[int, int], int] = {} + cont_uni = [0] * n_codigos + n = 0 + for seq in codigos_por_imagem: + for a, b in zip(seq, seq[1:]): + cont_bi[(a, b)] = cont_bi.get((a, b), 0) + 1 + cont_uni[a] += 1 + n += 1 + if n == 0: + return {"ppl_visual": None, "n": 0} + ce = 0.0 + for (a, b), c in cont_bi.items(): + p = (c + alfa) / (cont_uni[a] + alfa * n_codigos) + ce -= c * math.log(p) + return {"ppl_visual": round(math.exp(ce / n), 2), "n": n, + "codigos_distintos": len({b for _, b in cont_bi})} diff --git a/src/khtst/metricas/benchmarks.py b/src/khtst/metricas/benchmarks.py new file mode 100644 index 0000000000000000000000000000000000000000..5872824e9ae759b12de581b7cec92ee6c3887767 --- /dev/null +++ b/src/khtst/metricas/benchmarks.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- +"""HARNESS de benchmarks de ALTO NÍVEL (doc 18 §3.3) — proxies PT-BR honestos: + + MMMU (estilo) — múltipla escolha A–D; score = acurácia de alternativa. + MME/MM-Bench (estilo) — itens binários sim/não; score MME = 2·acc + acc⁺ + (acc⁺ = fração de imagens com TODAS as questões certas — fórmula + oficial do MME). + MathVista (estilo) — resposta numérica/exata com normalização PT-BR + (vírgula decimal → ponto, sem unidades); acurácia exata. + +Os itens são construídos do CORPUS PT-BR real (vqa/ocr) por protocolo padrão: +MCQ = resposta correta + 3 distratores amostrados de outras respostas +(posição aleatória com semente fixa); binários = respostas sim/não; numéricos += respostas parseáveis como número. RÓTULO OBRIGATÓRIO: 'proxy em escala +reduzida' — os benchmarks completos (MMMU 11.5k, MME 2.3k) exigem GPU. +""" +from __future__ import annotations + +import random +import re +from collections import defaultdict + +import torch + +_LETRA = re.compile(r"\b([A-Da-d])\b|^\s*([A-Da-d])[).:]") +_SIM_NAO = re.compile(r"\b(sim|não|nao)\b", re.IGNORECASE) +_NUM = re.compile(r"-?\d+(?:[.,]\d+)?") +# números escritos PT-BR (MathVista-proxy: respostas em palavras) +_NUM_PALAVRA = {"um": "1", "uma": "1", "dois": "2", "duas": "2", "tres": "3", + "quatro": "4", "cinco": "5", "seis": "6", "sete": "7", + "oito": "8", "nove": "9", "dez": "10", "onze": "11", + "doze": "12", "quinze": "15", "vinte": "20", + "cinquenta": "50", "cem": "100", "zero": "0"} +_STOP = {"de", "da", "do", "das", "dos", "em", "na", "no", "com", "uma", + "um", "para", "por", "que", "os", "as", "ao", "aos", "e", "a", "o"} + + +def _palavra_chave(caption: str) -> str | None: + """Palavra-significativo mais longa da caption (p/ verificação MME).""" + cand = [t for t in re.findall(r"[\wÀ-ÿ]+", caption.lower()) + if len(t) > 4 and t not in _STOP] + return max(cand, key=len) if cand else None + + +# ---------------- construção dos itens ---------------- +def construir_itens(registros, n_max: int = 40, semente: int = 2026) -> dict: + """Constrói os três conjuntos a partir do corpus (protocolo padrão).""" + rng = random.Random(semente) + vqa = [r for r in registros if r["tarefa"] == "vqa" + and (r.get("saida") or "").strip()] + ocr = [r for r in registros if r["tarefa"] == "ocr" + and (r.get("saida") or "").strip()] + pool = vqa + ocr + # v6 fix: percorre o POOL INTEIRO coletando cada tipo até a cota — as + # primeiras 30 respostas podem ser todas descritivas (MCQ) + mcq, binarios, numericos = [], [], [] + cota_mcq, cota_bin, cota_num = n_max, max(n_max // 2, 6), max(n_max // 3, 4) + respostas = [r["saida"].strip() for r in pool] + for r in pool[: n_max * 20]: + if len(mcq) >= cota_mcq and len(binarios) >= cota_bin \ + and len(numericos) >= cota_num: + break + # normaliza pontuação final ("não.", "12.") p/ classificação + resp = r["saida"].strip().strip(" .!?,;:") + resp_lower = resp.lower().replace("á", "a").replace("ã", "a") + entrada = r.get("entrada") or "Descreva a imagem." + if len(resp) <= 3 and resp_lower in ("sim", "nao"): + if len(binarios) < cota_bin: + binarios.append({"pergunta": entrada, + "resposta": "não" if resp_lower == "nao" else "sim", + "imagem": r.get("imagem"), + "tarefa": r["tarefa"], "id_img": id(r)}) + continue + resp_limpo = resp.replace(" ", "").replace("%", "") + resp_num = _NUM_PALAVRA.get(resp_lower) or ( + resp_limpo if _NUM.fullmatch(resp_limpo) else None) + if resp_num is not None: + if len(numericos) < cota_num: + numericos.append({"pergunta": entrada, "resposta": str(resp_num), + "imagem": r.get("imagem"), + "tarefa": r["tarefa"]}) + continue + if len(mcq) >= cota_mcq: + continue + distratores = set() + tentativas = 0 + while len(distratores) < 3 and tentativas < 50: + cand = rng.choice(respostas) + tentativas += 1 + if cand.lower() != resp.lower() and len(cand) < 60: + distratores.add(cand) + if len(distratores) < 3: + continue + ops = list(distratores) + pos = rng.randrange(4) + ops.insert(pos, resp) + mcq.append({"pergunta": entrada, "opcoes": ops, + "correta": "ABCD"[pos], "imagem": r.get("imagem"), + "tarefa": r["tarefa"]}) + # v6 — MME (estilo): além de respostas reais sim/não, VERIFICAÇÃO + # SINTETIZADA a partir das captions ground-truth (protocolo oficial do + # MME): 'Isso é uma imagem de X?' → sim (X da própria caption) / não + # (X de outra caption). Rotulado como proxy sintetizado. + caps = [r for r in registros if r["tarefa"] == "imagem_caption" + and (r.get("saida") or "").strip() and r.get("imagem")] + rng_c = random.Random(semente + 7) + chaves = [(_palavra_chave(r["saida"]), r) for r in caps] + chaves = [(k, r) for k, r in chaves if k] + faltam = cota_bin - len(binarios) + for k, r in chaves[: max(2 * faltam, 8)]: + if len(binarios) >= cota_bin: + break + outro = rng_c.choice(chaves)[0] + if not outro or outro == k: + continue + if rng_c.random() < 0.5: + binarios.append({"pergunta": f"Isso é uma imagem de {k}?", + "resposta": "sim", "imagem": r.get("imagem"), + "tarefa": "imagem_caption", "id_img": id(r), + "sintetizado": True}) + else: + binarios.append({"pergunta": f"Isso é uma imagem de {outro}?", + "resposta": "não", "imagem": r.get("imagem"), + "tarefa": "imagem_caption", "id_img": id(r), + "sintetizado": True}) + return {"mmmc": mcq, "mme": binarios, "mathvista": numericos} + + +# ---------------- utilidades ---------------- +def _prefixo_visual(modelo, item) -> torch.Tensor | None: + img = item.get("imagem") + if not img: + return None + try: + from khtst.treino.treinador import _imagem_para_tensor + lado = modelo.cfg.modelo.imagem["resolucao"] + tens = _imagem_para_tensor(img, lado).unsqueeze(0) + return modelo.codificar_multimodal({"imagem": tens}) + except Exception: + return None + + +def _gerar(modelo, tk, prompt: str, tarefa: str, item, + max_novos: int = 8) -> str: + ids = tk.encode(prompt, tarefa=tarefa, max_len=160) + ids_p = torch.tensor([ids]) if ids else torch.zeros(1, 1, dtype=torch.long) + from khtst.treino.treinador import TAREFAS_IMAGEM, TAREFAS_AUDIO + emb = _prefixo_visual(modelo, item) if item.get("imagem") else None + novos = modelo.gerar(ids_p, max_novos=max_novos, temperatura=0.7, + top_p=0.9, emb_prefixo=emb) + return tk.decodificar(novos) if hasattr(tk, "decodificar") else tk.decode(novos) + + +# ---------------- MMMU (estilo) ---------------- +def avaliar_mmcq(modelo, tk, itens: list[dict], n_max: int | None = None) -> dict: + """MMMU-proxy: prompt com A–D; resposta = primeira letra A–D gerada.""" + itens = itens[:n_max] if n_max else itens + acertos = 0 + por_letra = defaultdict(int) + for it in itens: + linhas = "\n".join(f"{l}) {o}" for l, o in zip("ABCD", it["opcoes"])) + prompt = (f"Pergunta: {it['pergunta']}\n{linhas}\n" + f"Responda com a letra correta (A, B, C ou D). Resposta:") + txt = _gerar(modelo, tk, prompt, it.get("tarefa", "vqa"), it) + m = _LETRA.search(txt[:24]) + letra = (m.group(1) or m.group(2)).upper() if m else "" + por_letra[letra or "∅"] += 1 + acertos += int(letra == it["correta"]) + n = len(itens) + return {"benchmark": "MMMU-proxy-PT", "n": n, + "acuracia": round(acertos / n, 4) if n else None, + "acertos": acertos, + "distribuicao": dict(por_letra), + "acaso": 0.25, "rotulo": "proxy em escala reduzida"} + + +# ---------------- MME / MM-Bench (estilo) ---------------- +def avaliar_mme(modelo, tk, itens: list[dict], n_max: int | None = None) -> dict: + """MME-proxy: itens sim/não; score = 2·acc + acc⁺ (fórmula oficial).""" + itens = itens[:n_max] if n_max else itens + acertos = 0 + por_imagem: dict[int, list[bool]] = defaultdict(list) + for it in itens: + prompt = (f"Pergunta: {it['pergunta']}\n" + f"Responda apenas 'sim' ou 'não'. Resposta:") + txt = _gerar(modelo, tk, prompt, it.get("tarefa", "vqa"), it, + max_novos=6) + m = _SIM_NAO.search(txt) + resp = m.group(0).lower() if m else "" + resp = "não" if resp in ("nao",) else resp + ok = int(resp == it["resposta"]) if it["resposta"] in ("sim", "não") \ + else 0 + acertos += ok + por_imagem[it.get("id_img", id(it))].append(bool(ok)) + n = len(itens) + acc = acertos / n if n else 0.0 + acc_mais = (sum(1 for oks in por_imagem.values() if all(oks)) + / len(por_imagem)) if por_imagem else 0.0 + return {"benchmark": "MME-proxy-PT", "n": n, "acc": round(acc, 4), + "acc_mais": round(acc_mais, 4), + "score_mme": round(2 * acc + acc_mais, 4) if n else None, + "rotulo": "proxy em escala reduzida"} + + +# ---------------- MathVista (estilo) ---------------- +def _normalizar_num(s: str) -> str | None: + m = _NUM.search(s.replace(" ", "")) + if not m: + return None + return m.group(0).replace(".", "").replace(",", ".") \ + if re.match(r"^\d{1,3}(\.\d{3})+(,\d+)?$", m.group(0)) \ + else m.group(0).replace(",", ".") + + +def avaliar_mathvista(modelo, tk, itens: list[dict], + n_max: int | None = None) -> dict: + """MathVista-proxy: acerto exato após normalização numérica PT-BR.""" + itens = itens[:n_max] if n_max else itens + acertos = 0 + for it in itens: + prompt = (f"Pergunta (resposta numérica): {it['pergunta']}\nResposta:") + txt = _gerar(modelo, tk, prompt, it.get("tarefa", "ocr"), it, + max_novos=10) + alvo = _normalizar_num(it["resposta"]) + pred = _normalizar_num(txt) + acertos += int(alvo is not None and pred == alvo) + n = len(itens) + return {"benchmark": "MathVista-proxy-PT", "n": n, + "acuracia": round(acertos / n, 4) if n else None, + "acertos": acertos, "rotulo": "proxy em escala reduzida"} + + +def avaliar_todos(modelo, tk, registros, n_max: int = 40) -> dict: + """Pacote completo de benchmarks sobre o corpus PT-BR.""" + conj = construir_itens(registros, n_max=n_max) + return { + "mmmu": avaliar_mmcq(modelo, tk, conj["mmmc"]), + "mme": avaliar_mme(modelo, tk, conj["mme"]), + "mathvista": avaliar_mathvista(modelo, tk, conj["mathvista"]), + } diff --git a/src/khtst/metricas/geracao_texto.py b/src/khtst/metricas/geracao_texto.py new file mode 100644 index 0000000000000000000000000000000000000000..c6eea7b00723bd4195369402ddf19965206ebe39 --- /dev/null +++ b/src/khtst/metricas/geracao_texto.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +"""Métricas de GERAÇÃO DE TEXTO (doc 18 §3.2) — implementação pura (sem deps): + + BLEU 1–4 (Papineni et al. 2002) — precisões com clipping + brevity penalty. + ROUGE-L (Lin 2004) — LCS com F1 (β=1.2 conforme implementação oficial). + METEOR (Banerjee & Lavie 2005) — P/R harmônica (10PR/(P+9R)), matching + EXATO + STEM PT-BR leve + sinônimos mínimos, penalidade de + fragmentação γ·(frag/match)³. Simplificação documentada: sem + WordNet (EN); tabela determinística PT-BR. + CIDEr (Vedantam et al. 2015) — cosseno TF-IDF de n-gramas (n≤4) contra + m referências, df calculado no corpus de referências. + +Todas as funções recebem TEXTO (tokenização por whitespace + lowercase) e são +determinísticas/reproduzíveis. +""" +from __future__ import annotations + +import math +import re +from collections import Counter + +_TOK = re.compile(r"[\wÀ-ÿ]+", re.UNICODE) + + +def tokenizar(s: str) -> list[str]: + return _TOK.findall(s.lower()) + + +def _ngramas(toks: list[str], n: int) -> Counter: + return Counter(tuple(toks[i:i + n]) for i in range(len(toks) - n + 1)) + + +# ---------------- BLEU ---------------- +def bleu(hip: str, refs: list[str], n_max: int = 4) -> dict: + """BLEU-n (média geométrica das precisões 1..n_max com clipping) + BP.""" + h = tokenizar(hip) + rs = [tokenizar(r) for r in refs] + log_prec, vals = 0.0, {} + for n in range(1, n_max + 1): + ch = _ngramas(h, n) + if not ch: + vals[f"p{n}"] = 0.0 + log_prec += (1.0 / n_max) * math.log(1e-9) + continue + clip = 0 + total = sum(ch.values()) + for r in rs: + cr = _ngramas(r, n) + clip = max(clip, sum(min(c, cr[g]) for g, c in ch.items())) + p = clip / total + vals[f"p{n}"] = round(p, 6) + log_prec += (1.0 / n_max) * math.log(max(p, 1e-9)) + melhor_r = max((len(r) for r in rs), default=0) + bp = min(1.0, math.exp(1.0 - melhor_r / len(h))) if h and melhor_r else 0.0 + return {"bleu": round(bp * math.exp(log_prec), 6), "bp": round(bp, 6), **vals} + + +# ---------------- ROUGE-L ---------------- +def _lcs(a: list[str], b: list[str]) -> int: + if not a or not b: + return 0 + prev = [0] * (len(b) + 1) + for i in range(1, len(a) + 1): + cur = [0] * (len(b) + 1) + ai = a[i - 1] + for j in range(1, len(b) + 1): + cur[j] = prev[j - 1] + 1 if ai == b[j - 1] else max(prev[j], cur[j - 1]) + prev = cur + return prev[-1] + + +def rouge_l(hip: str, refs: list[str]) -> dict: + """ROUGE-L: F = (1+β²)RP/(R+P+β²R·P) com β=1.2; melhor F entre refs.""" + h = tokenizar(hip) + melhor = {"rouge_l": 0.0, "p": 0.0, "r": 0.0, "lcs": 0} + for r in refs: + rr = tokenizar(r) + lcs = _lcs(h, rr) + if not h or not rr: + continue + p = lcs / len(h) + rec = lcs / len(rr) + beta = 1.2 + f = (1 + beta * beta) * p * rec / max(p + rec + beta * beta * p * rec, 1e-12) + if f > melhor["rouge_l"]: + melhor = {"rouge_l": round(f, 6), "p": round(p, 6), + "r": round(rec, 6), "lcs": lcs} + return melhor + + +# ---------------- METEOR (PT-BR leve) ---------------- +_SUFX = ("icamente", "amente", "mente", "idades", "idade", "amento", + "amentos", "ação", "ações", "ção", "ções", "sões", "zinho", + "zinha", "inho", "inha", "íssimo", "íssima", "eza", "ista", + "izar", "ificar") + + +def stem_pt(p: str) -> str: + """Stemmer leve PT-BR (determinístico): remove sufixos comuns > 4 chars.""" + if len(p) > 5: + for s in _SUFX: + if p.endswith(s) and len(p) - len(s) >= 4: + return p[: len(p) - len(s)] + return p[:-1] if len(p) > 4 and p.endswith("s") else p + + +SINONIMOS = { + "bonito": {"belo", "lindo", "elegante"}, "rápido": {"veloz", "ligeiro"}, + "grande": {"enorme", "amplo", "vasto"}, "feliz": {"alegre", "contente"}, + "casa": {"lar", "moradia", "residência"}, "carro": {"automóvel", "veículo"}, + "falar": {"dizer", "contar", "comentar"}, "ver": {"olhar", "observar"}, + "comer": {"ingerir", "alimentar"}, "bom": {"ótimo", "excelente", "bom"}, +} + + +def _classe(p: str) -> frozenset[str]: + return frozenset({p} | SINONIMOS.get(p, set()) | {stem_pt(p)}) + + +def meteor(hip: str, ref: str) -> dict: + """METEOR com matching exato→stem→sinônimo (em ordem), alinhamento + guloso monotônico, F-mean 10PR/(P+9R) e penalidade de fragmentação.""" + h = tokenizar(hip) + r = tokenizar(ref) + usados_h, usados_r = set(), set() + match: list[tuple[int, int]] = [] + for modo in ("exato", "stem", "sinonimo"): + mapa_r = {} + for j, tok in enumerate(r): + if j in usados_r: + continue + chave = tok if modo == "exato" else \ + (stem_pt(tok) if modo == "stem" else None) + if chave is not None: + mapa_r.setdefault(chave, j) + for i, tok in enumerate(h): + if i in usados_h: + continue + chave = tok if modo == "exato" else \ + (stem_pt(tok) if modo == "stem" else None) + j = mapa_r.get(chave) if chave else None + if j is None and modo == "sinonimo": + c_h = _classe(tok) + for j2, tok2 in enumerate(r): + if j2 in usados_r: + continue + if _classe(tok2) & c_h: + j = j2 + break + if j is not None: + usados_h.add(i) + usados_r.add(j) + match.append((i, j)) + m = len(match) + if m == 0: + return {"meteor": 0.0, "p": 0.0, "r": 0.0, "matches": 0, "frag": 0.0} + p = m / len(h) if h else 0.0 + rec = m / len(r) if r else 0.0 + fm = 10 * p * rec / max(p + 9 * rec, 1e-12) + ordem = [j for _, j in sorted(match)] + frags = 1 + sum(1 for a, b in zip(ordem, ordem[1:]) if b != a + 1) + pen = (frags / m) ** 3 if m else 0.0 + return {"meteor": round(fm * (1 - pen), 6), "p": round(p, 6), + "r": round(rec, 6), "matches": m, "frag": round(pen, 6)} + + +# ---------------- CIDEr ---------------- +def cider(hip: str, refs: list[str], df_corpus: Counter | None = None, + n_docs: int | None = None, n_max: int = 4) -> dict: + """CIDEr: g_k = TF·IDF de cada n-grama (n≤4), cosseno contra cada ref; + df padrão: corpus de referências (1 doc por ref). Fórmula oficial.""" + rs = [tokenizar(r) for r in refs] + h = tokenizar(hip) + m = len(rs) + if m == 0 or not h: + return {"cider": 0.0} + n_docs = n_docs or m + df_corpus = df_corpus if df_corpus is not None else Counter() + if not df_corpus: + for r in rs: + for n in range(1, n_max + 1): + for g in set(_ngramas(r, n)): + df_corpus[g] += 1 + + def vetor(toks: list[str]) -> dict: + v = {} + for n in range(1, n_max + 1): + c = _ngramas(toks, n) + for g, cnt in c.items(): + tf = cnt / max(1, len(toks)) + idf = math.log(n_docs / (df_corpus.get(g, 0) + 1e-9)) + v[(n, g)] = tf * max(idf, 1e-9) * (1.0 / n_max) + return v + + vh = vetor(h) + if not vh: + return {"cider": 0.0} + soma = 0.0 + for r in rs: + vr = vetor(r) + num = sum(vh[g] * vr.get(g, 0.0) for g in vh) + nh = math.sqrt(sum(x * x for x in vh.values())) + nr = math.sqrt(sum(x * x for x in vr.values())) or 1e-9 + soma += num / (nh * nr) + return {"cider": round(soma / m, 6)} + + +# ---------------- pacote ---------------- +def pacote_completo(hip: str, refs: list[str], df_corpus: Counter | None = None, + n_docs: int | None = None) -> dict: + """Todas as métricas de geração para UMA hipótese + referências.""" + out = {} + out.update(bleu(hip, refs)) + out.update(rouge_l(hip, refs)) + out.update(meteor(hip, refs[0] if refs else "")) + out.update(cider(hip, refs, df_corpus=df_corpus, n_docs=n_docs)) + return out diff --git a/src/khtst/metricas/som_metricas.py b/src/khtst/metricas/som_metricas.py new file mode 100644 index 0000000000000000000000000000000000000000..10f118764cd2c0699e360bdd35ece44bf604f5ee --- /dev/null +++ b/src/khtst/metricas/som_metricas.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +"""Métricas de QUALIDADE, DINÂMICA e ESPAÇO LATENTE dos mapas Kohonen (doc 18 §3.4). + +Qualidade: QE (doc 01 §5), TE (Kiviluoto), MEDIDA DE DISTORÇÃO (eq. 18.3 — + média ponderada pela função de vizinhança). +Dinâmica: σ(t), α(t) das agendas; WEIGHT CODEBOOK DRIFT ‖W_t−W_{t−1}‖_F/‖W_{t−1}‖_F. +Espaço: FREQUÊNCIA DE ATIVAÇÃO por neurônio (+ entropia + rank efetivo) + e U-MATRIX (distância média entre vizinhos imediatos da grade). + +Todas operam sobre QUALQUER objeto com interface SOMKohonen (w, pos, uso_total, +hit_ema) — inclui as 8 variantes via camada interna delegada. +""" +from __future__ import annotations + +import math + +import torch + + +# ---------------- qualidade ---------------- +def erro_quantizacao(camada, x: torch.Tensor) -> float: + """QE = média do d² do BMU por amostra.""" + c, d2 = camada.bmu(x) + return float(d2.mean()) + + +def erro_topologico(camada, x: torch.Tensor) -> float: + """TE (Kiviluoto): fração de amostras cujos 2 BMUs não são adjacentes.""" + d2 = torch.cdist(x, camada.w).pow(2) + dois = d2.topk(2, dim=1, largest=False).indices + dist = (camada.pos[dois[:, 0]] - camada.pos[dois[:, 1]]).pow(2).sum(dim=1) + return float((dist > 1.5).float().mean()) + + +def medida_distorcao(camada, x: torch.Tensor, sigma_eval: float = 1.0) -> float: + """DISTORÇÃO (eq. 18.3): E = (1/N)Σ_i Σ_j h_{c*,j}‖x_i−w_j‖² — global, + ponderada pela função de vizinhança gaussiana com σ de avaliação fixo.""" + d2 = torch.cdist(x, camada.w).pow(2) # (N, K) + c = d2.argmin(dim=1) + h_mat = torch.exp(-torch.cdist(camada.pos, camada.pos[c, :]).pow(2) + / (2 * sigma_eval ** 2 + 1e-12)) # (K, N) + e_i = (h_mat * d2.T).sum(dim=0) # (N,) + return float(e_i.mean()) + + +# ---------------- dinâmica ---------------- +def agenda_sigma(camada) -> tuple[float, float]: + """σ inicial e final configurados (agenda exponencial do doc 01 §2).""" + return float(getattr(camada, "sigma0", 0.0)), float(getattr(camada, "sigma_f", 0.0)) + + +def peso_codebook_drift(w_atual: torch.Tensor, w_anterior: torch.Tensor) -> float: + """ΔW = ‖W_t − W_{t−1}‖_F / (‖W_{t−1}‖_F + ε) — convergência ⇒ ΔW → 0.""" + den = float(w_anterior.norm()) + 1e-12 + return float((w_atual - w_anterior).norm() / den) + + +# ---------------- espaço latente ---------------- +def frequencia_ativacao(camada, x: torch.Tensor | None = None) -> dict: + """Histograma π_j de ativação + entropia + rank efetivo exp(H). + Fonte: uso_total quando existir; fallback hit_ema; fallback final: + bincount dos BMUs de x (variantes de grafo sem contadores — GCS).""" + uso = getattr(camada, "uso_total", None) + if uso is None or float(uso.sum()) <= 0.0: + he = getattr(camada, "hit_ema", None) + if he is not None and float(he.sum()) > 0.0: + uso = he.clamp(min=0).to(torch.float64) + elif x is not None and hasattr(camada, "w"): + c = torch.cdist(x, camada.w).argmin(dim=1) + uso = torch.bincount(c, minlength=camada.w.shape[0]).to(torch.float64) + else: + uso = None + if uso is None: + uso = torch.zeros(getattr(camada, "w").shape[0]).to(torch.float64) \ + if hasattr(camada, "w") else torch.zeros(1).to(torch.float64) + uso = uso.to(torch.float64) + total = float(uso.sum()) + if total <= 0.0: + return {"n_ativos": 0, "entropia": 0.0, "rank_efetivo": 0.0, + "frac_vivas": 0.0} + pi = uso / total + nz = pi[pi > 0] + h = float(-(nz * nz.log()).sum()) + return {"n_ativos": int((uso > 0).sum()), + "entropia": round(h, 6), + "rank_efetivo": round(math.exp(h), 4), + "frac_vivas": round(float((uso > 0).float().mean()), 4)} + + +def u_matrix(camada) -> dict: + """U-MATRIX: u_j = média ‖w_j − w_k‖ sobre vizinhos imediatos na grade + (8-vizinhança com raio na grade ≤ 1.5). Alta ⇒ fronteira semântica.""" + k = camada.w.shape[0] + pos, w = camada.pos, camada.w + u = torch.zeros(k) + viz: list[list[int]] = [[] for _ in range(k)] + for j in range(k): + d2g = (pos - pos[j]).pow(2).sum(dim=1) + idx = (d2g <= 1.5).nonzero(as_tuple=True)[0] + idx = idx[idx != j] + viz[j] = idx.tolist() + if idx.numel(): + u[j] = torch.cdist(w[j:j + 1], w[idx]).squeeze(0).mean() + picos = 0 + for j in range(k): + if viz[j] and u[j] >= max(u[i] for i in viz[j]) and u[j] > 0: + picos += 1 + return {"u_media": round(float(u.mean()), 6), + "u_max": round(float(u.max()), 6), + "u_std": round(float(u.std()), 6), + "fronteiras_picos": picos, + "vetor": [round(float(v), 5) for v in u.tolist()]} + + +# ---------------- pacote por variante ---------------- +def _bmu_generico(camada, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """BMU por cdist direta — para variantes sem método bmu (GCS: grafo, não + grade). d² = ‖x − w_j‖²; retorno compatível com SOMKohonen.bmu.""" + d2 = torch.cdist(x, camada.w).pow(2) + minimo, c = d2.min(dim=1) + return c, minimo + + +def _pos_generico(camada) -> torch.Tensor: + """Posições de grade: a da camada se existir; senão grade LINEAR 1-D + (documentado: GCS tem topologia de grafo — aproximação linear para + TE/U-Matrix; proporção honesta do relatório).""" + if hasattr(camada, "pos") and getattr(camada, "pos") is not None: + return camada.pos + k = camada.w.shape[0] + return torch.arange(k, dtype=torch.float32).unsqueeze(1).repeat(1, 2) * 0.0 \ + + torch.arange(k, dtype=torch.float32).unsqueeze(1) + + +def metricas_variante(nome: str, objeto, x: torch.Tensor, + w_anterior: torch.Tensor | None = None) -> dict: + """Pacote completo para UMA variante (delega à camada SOM interna quando + hierárquica/wrapped: H-SOM → n1; CPN → kohonen). Fallbacks: bmu por + cdist e grade linear quando a variante não fornece (GCS).""" + camada = getattr(objeto, "kohonen", None) or getattr(objeto, "n1", None) or objeto + if not hasattr(camada, "w"): + return {"erro": "sem pesos"} + pos_original = getattr(camada, "pos", None) + if pos_original is None: + camada.pos = _pos_generico(camada) # só para TE/U-matrix desta aval + try: + d2 = torch.cdist(x, camada.w).pow(2) + out = {"k": int(camada.w.shape[0]), + "qe": round(float(d2.min(dim=1).values.mean()), 6)} + out["te"] = round(erro_topologico(camada, x), 6) + out["distorcao"] = round(medida_distorcao(camada, x), 6) + s0, sf = agenda_sigma(camada) + out["sigma0"], out["sigma_f"] = round(s0, 4), round(sf, 4) + alpha0 = float(getattr(camada, "alpha0", 0.0)) + passo = int(getattr(camada, "passo", 0) or 0) + out["alpha0"] = round(alpha0, 4) + out["alpha_ultimo"] = round(alpha0 / (1.0 + 0.01 * max(passo, 1)), 6) + if w_anterior is not None and w_anterior.shape == camada.w.shape: + out["codebook_drift"] = round(peso_codebook_drift(camada.w, w_anterior), 6) + out.update(frequencia_ativacao(camada, x)) + out["u_matrix_resumo"] = {k2: v for k2, v in u_matrix(camada).items() + if k2 != "vetor"} + out["taxa_ativos_ema"] = (round(float((camada.hit_ema >= camada.eps_ativo) + .float().mean()), 4) + if hasattr(camada, "hit_ema") else None) + finally: + if pos_original is None and hasattr(camada, "pos"): + del camada.pos # restaura estado original + return out diff --git a/src/khtst/nlg/__init__.py b/src/khtst/nlg/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2e8635370e4f1ba430b387e2fe715302fe2114db --- /dev/null +++ b/src/khtst/nlg/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +"""Pacote NLG (geração) — v4. UnidadeNLG modula o estado oculto na geração +(plano, estilo, re-ranking de coerência) e o DecodificadorEspeculativoMedusa +acelera a decodificação com inocuidade comprovada (doc 14, Teorema 14.1).""" +from khtst.nlg.decodificador_especulativo import (DecodificadorEspeculativoMedusa, + StatsEspeculativo) +from khtst.nlg.unidade_nlg import UnidadeNLG + +__all__ = ["UnidadeNLG", "DecodificadorEspeculativoMedusa", "StatsEspeculativo"] diff --git a/src/khtst/nlg/decodificador_especulativo.py b/src/khtst/nlg/decodificador_especulativo.py new file mode 100644 index 0000000000000000000000000000000000000000..71d3df39e0a965665507c405d4ee5e4ddcf0a9c2 --- /dev/null +++ b/src/khtst/nlg/decodificador_especulativo.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +"""Decodificador especulativo em ÁRVORE (Medusa) — v4 (doc 14). + +Absorvido de gru-ring-v13-9-2: + • medusa_speculative_decoder.py — verificação em UM forward, correção residual, + EMA de estatísticas (0.9/0.1), guarda contra laço infinito; + • multi_token_predictor.py — poda por cabeça c_k=min(c, max(1, c−k)), + aceitação TÍPICA determinística (Heim 2022, eq. 14.2). + +Teorema 14.1 (inocuidade): a política com correção pelo alvo preserva +exatamente p_alvo — a especulação nunca muda a distribuição, só o custo. +Teorema 14.2: speedup S = (E[aceitos]+1)/(1+|T|/V_ef) > 1 sse ρ > |T|/(V_ef+|T|). + +Diferenças do alvo de estudo (doc 14 §3): rascunho com logits EMPATADOS +(K·d²), verificação com a MESMA política de punição/filtro da geração normal, +aceitação típica com log-softmax finito (asserção do Agente Engenheiro). +""" +from __future__ import annotations + +from dataclasses import dataclass, field + +import torch +import torch.nn.functional as F + + +@dataclass +class StatsEspeculativo: + n_chamadas: int = 0 + tokens_por_passo: float = 0.0 + tamanho_arvore_medio: float = 0.0 + aceitacao_media: float = 0.0 # ρ̂ (EMA 0.9/0.1 — absorvida) + speedup_estimado: float = 1.0 + + +class DecodificadorEspeculativoMedusa: + """Rascunho = MTP (K cabeças, logits empatados); alvo = modelo KHTST.""" + + def __init__(self, modelo, mtp, c_por_cabeca: int = 4, + tau_tipico: float = 0.95, teto_arvore: int = 10): + self.modelo = modelo + self.mtp = mtp + self.c = c_por_cabeca + self.tau = tau_tipico + self.teto = teto_arvore + self.stats = StatsEspeculativo() + + # ---------------- aceitação típica (eq. 14.2) ---------------- + @staticmethod + def aceitacao_tipica(logits: torch.Tensor, candidatos: torch.Tensor, + tau: float = 0.95) -> torch.Tensor: + """logits: (V,) do ALVO; candidatos: (C,) do rascunho → máscara bool. + Aceita se −log p(t) − H ≤ τ·H (conjunto típico, doc 14 §3).""" + log_p = F.log_softmax(logits.float(), dim=-1) + assert torch.isfinite(log_p).all(), \ + "BUG (doc 14.4): log-softmax não finito na aceitação típica" + p = log_p.exp() + H = float(-(p * log_p).sum()) + neg_log_p = -log_p[candidatos] + return (neg_log_p - H) <= tau * max(H, 1e-6) + + # ---------------- poda por cabeça (doc 14 §2) ---------------- + def podar_cabecas(self, logits_rascunho: list[torch.Tensor]) -> list[int]: + """c_k = min(c, max(1, c−k)) candidatos top-c por cabeça k.""" + candidatos: list[int] = [] + for k, lg in enumerate(logits_rascunho): + lg = lg.view(-1) # (B,V)/(1,1,V) → (V,) [B=1] + take = min(self.c, max(1, self.c - k)) + top = lg.topk(take).indices.tolist() + for t in top: + if t not in candidatos: # árvore sem duplicatas + candidatos.append(t) + if len(candidatos) >= self.teto: + break + return candidatos[: self.teto] + + # ---------------- geração com verificação em árvore ---------------- + @torch.no_grad() + def gerar(self, ids_prompt: list[int], max_novos: int = 48, + temperatura: float = 0.8, top_k: int = 20, + top_p: float | None = 0.9, punicao_repeticao: float = 1.2, + id_eos: int = 2) -> tuple[list[int], StatsEspeculativo]: + """Retorna (novos_ids, stats). Mesma política de amostragem do alvo + (doc 14 §3.3) para que o Teorema 14.1 permaneça exato.""" + ids = list(ids_prompt) + novos: list[int] = [] + V = self.modelo.cfg.modelo.vocab + while len(novos) < max_novos: + # 1) rascunho: forward completo uma vez (CPU: V_ef=1 por passo) + t_ids = torch.tensor([ids]) + logits_alvo, oculto = self._forward_alvo(t_ids, len(ids)) + logits_alvo_ultimo = logits_alvo[0, -1] + # 2) K cabeças do MTP sobre o MESMO estado oculto + lista_logits = self.mtp.logits_rascunho(oculto[:, -1]) + candidatos = self.podar_cabecas(lista_logits) + if not candidatos: + break + n_arvore = len(candidatos) + # 3) verificação em UM passo: compara com a política do alvo + logits_filtrados = self._politica_alvo( + logits_alvo_ultimo, ids, temperatura, top_k, top_p, + punicao_repeticao) + # aceitação típica sobre a distribuição do ALVO (eq. 14.2 usa + # p_alvo — os logits filtrados têm -inf e não são distribuição) + mascara_tip = self.aceitacao_tipica( + logits_alvo_ultimo, torch.tensor(candidatos), self.tau) + # 4) aceita o maior prefixo cujos tokens são tip-aceitos E + # concordam com o argmax do alvo (verificação determinística) + argmax_alvo = int(logits_filtrados.argmax()) + aceitos = 0 + for k, t in enumerate(candidatos): + if bool(mascara_tip[k]) or t == argmax_alvo: + if k == 0 or True: # prefixo: aceita em sequência + aceitos += 1 + novos.append(t) + ids.append(t) + if len(novos) >= max_novos or t == id_eos: + break + else: + break + # 5) correção residual: se o argmax não foi aceito, injeta-o + if (aceitos == 0 or argmax_alvo not in novos[-aceitos:]) \ + and argmax_alvo != id_eos and len(novos) < max_novos \ + and aceitos == 0: + novos.append(argmax_alvo) + ids.append(argmax_alvo) + # 6) telemetria EMA (absorvida: 0.9/0.1) + self.stats.n_chamadas += 1 + ganho = aceitos + (0 if aceitos else 1) + self.stats.tokens_por_passo = (ganho if self.stats.n_chamadas == 1 + else 0.9 * self.stats.tokens_por_passo + + 0.1 * ganho) + self.stats.tamanho_arvore_medio = ( + float(n_arvore) if self.stats.n_chamadas == 1 + else 0.95 * self.stats.tamanho_arvore_medio + 0.05 * n_arvore) + self.stats.aceitacao_media = ( + aceitos / max(1, n_arvore) if self.stats.n_chamadas == 1 + else 0.9 * self.stats.aceitacao_media + + 0.1 * (aceitos / max(1, n_arvore))) + c_verificar = n_arvore / V + self.stats.speedup_estimado = ( + self.stats.tokens_por_passo / (1.0 + c_verificar)) + # 7) guarda de progresso (absorvida do decoder de estudo) + if aceitos == 0 and novos and novos[-1] == argmax_alvo: + pass # correção injetou progresso — continua + elif aceitos == 0: + break + if novos and novos[-1] == id_eos: + break + return novos, self.stats + + # ---------------- helpers ---------------- + def _forward_alvo(self, ids: torch.Tensor, n: int): + """Forward do tronco comgradiente desligado; retorna (logits, oculto).""" + for bloco in self.modelo.blocos: + bloco.reset_estado() + x = self.modelo.emb(ids) + for bloco in self.modelo.blocos: + x, _, _ = bloco(x) + oculto = self.modelo.norm_f(x) + logits = self.modelo.lm_head(oculto) + return logits, oculto + + def _politica_alvo(self, logits: torch.Tensor, contexto: list[int], + temperatura: float, top_k: int, + top_p: float | None, punicao: float) -> torch.Tensor: + """Aplica temperatura + punição dinâmica + top-k/top-p (mesma política + de `KHTSTModel.gerar` — doc 10 §8) para manter a equivalência 14.1.""" + V = logits.shape[-1] + lg = logits / max(temperatura, 1e-3) + contagens = torch.bincount(torch.tensor(contexto, dtype=torch.long), + minlength=V) + probs_ctx = contagens.float() / max(float(contagens.sum()), 1.0) + p_nz = probs_ctx[probs_ctx > 0] + H_ctx = float(-(p_nz * torch.log(p_nz + 1e-8)).sum()) if p_nz.numel() else 0.0 + fator = min(1.0, H_ctx / 10.0) + pen = 1.0 + (punicao - 1.0) * fator * (contagens > 0).float() + lg = lg / pen.unsqueeze(0) + if top_k and top_k < V: + topo = lg.topk(top_k, dim=-1) + lg = torch.full_like(lg, float("-inf")).scatter_( + -1, topo.indices, topo.values) + if top_p is not None and 0.0 < top_p < 1.0: + ordenados, idx = torch.sort(lg, descending=True, dim=-1) + probs = torch.softmax(ordenados, dim=-1) + acum = probs.cumsum(dim=-1) + manter = (acum - probs) < top_p + ordenados = torch.where(manter, ordenados, + torch.full_like(ordenados, float("-inf"))) + lg = torch.full_like(lg, float("-inf")).scatter_(-1, idx, ordenados) + return lg diff --git a/src/khtst/nlg/unidade_nlg.py b/src/khtst/nlg/unidade_nlg.py new file mode 100644 index 0000000000000000000000000000000000000000..aa2b58da41f5648973cbbfc77830a409fcea0f1a --- /dev/null +++ b/src/khtst/nlg/unidade_nlg.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +"""UnidadeNLG — especialista de GERAÇÃO (doc 15 §1) — v4. + +(i) vetor de PLANO (tema/estilo) por pooling do prompt; +(ii) RE-RANKING de coerência: penaliza bigramas não vistos no contexto + (tabela EMA de bigramas — dinâmica, O(|V_cand|)); +(iii) condicionamento de ESTILO: h_dec = h + W_estilo p, p ∈ R^8. + +Sem parâmetros da lm_head — a NLG MODULA o estado oculto (decisão doc 15 §1.iii). +""" +from __future__ import annotations + +import torch +import torch.nn as nn +import torch.nn.functional as F + +N_EIXOS_ESTILO = 8 + + +class UnidadeNLG(nn.Module): + def __init__(self, d: int = 192, n_estilos: int = N_EIXOS_ESTILO, + tam_bigrama_tab: int = 4096): + super().__init__() + self.d = d + # (i) plano: pooling do prompt + projeção + self.pooler = nn.Linear(d, d, bias=False) + # (iii) estilo: 8 eixos condicionais (logits de controle aprendíveis) + self.eixos = nn.Parameter(torch.zeros(n_estilos)) + self.W_estilo = nn.Linear(n_estilos, d, bias=False) + nn.init.zeros_(self.W_estilo.weight) # Teorema 15.1: nasce neutro + # (ii) tabela EMA de bigramas (hashing trick) p/ re-ranking de coerência + self.tam_tab = tam_bigrama_tab + self.register_buffer("tab_bigramas", torch.zeros(tam_bigrama_tab)) + self.beta_tab = 0.98 + self.ultimo_score_coerencia = 0.0 + + # ---------------- plano e condicionamento ---------------- + def plano(self, h_prompt: torch.Tensor) -> torch.Tensor: + """h_prompt: (B, Tp, d) → vetor de plano (B, d).""" + pesos = torch.softmax(self.pooler(h_prompt).mean(-1), dim=-1) # (B, Tp) + return (pesos.unsqueeze(-1) * h_prompt).sum(1) # (B, d) + + def condicionar(self, h: torch.Tensor, estilo: torch.Tensor | None = None) \ + -> torch.Tensor: + """Aplica h + W_estilo·p. estilo: (n_estilos) em [0,1]; default = σ(eixos).""" + p = torch.sigmoid(self.eixos) if estilo is None else estilo + return h + self.W_estilo(p).view(1, 1, -1) + + # ---------------- v5: enriquecimento de prompt (doc 17 §3) ---------------- + EIXOS_FRASES = ( + ("com detalhes nítidos", "com precisão de traços"), + ("com composição harmônica", "com equilíbrio visual"), + ("com iluminação suave", "com luz natural"), + ("com cores vibrantes", "com paleta rica"), + ("em primeiro plano", "em destaque"), + ("com profundidade de campo", "com perspectiva clara"), + ("com textura realista", "com materiais bem definidos"), + ("em alta qualidade", "com acabamento refinado"), + ) + + @torch.no_grad() + def enriquecer_prompt(self, prompt: str, limiar: float = 0.5) -> str: + """v5 — enriquece o prompt p/ difusão com qualificadores PT-BR + selecionados pelos eixos de estilo (doc 17 §3, Teorema 17.3: mapeamento + determinístico e reversível dos eixos → linguagem; sem novos + parâmetros — reusa os eixos aprendidos).""" + p_eixos = torch.sigmoid(self.eixos).tolist() + frases = [frases_i[int(a >= limiar)] + for a, (frases_i,) in + zip(p_eixos, [(f,) for f in self.EIXOS_FRASES])] + escolhas = [frase for a, frase in zip(p_eixos, self.EIXOS_FRASES) + if a >= limiar for frase in (frase[int(a >= 0.75)],)] + base = prompt.strip().rstrip(".") + if not escolhas: + return f"{base}, em alta qualidade" + return f"{base}, " + ", ".join(escolhas[:3]) + + # ---------------- re-ranking de coerência ---------------- + @torch.no_grad() + def atualizar_bigramas(self, ids: torch.Tensor): + """Observa bigramas do texto (ids: (T,)) na tabela EMA.""" + for t in range(len(ids) - 1): + chave = self._hash(int(ids[t]), int(ids[t + 1])) + self.tab_bigramas[chave] = (self.beta_tab * self.tab_bigramas[chave] + + (1 - self.beta_tab) * 1.0) + + def _hash(self, a: int, b: int) -> int: + return (a * 1000003 + b) % self.tam_tab + + @torch.no_grad() + def rerank_coerencia(self, logits: torch.Tensor, contexto: list[int], + peso: float = 0.35, teto: int = 200) -> torch.Tensor: + """Penaliza candidatos cujo bigrama (último token do contexto, candidato) + é INÉDITO no contexto recente — doc 15 §1.ii. logits: (V,) ou (1, V).""" + if len(contexto) == 0: + return logits + plano = logits.view(-1) # (V,) — funciona p/ (1,V) + ultimo = contexto[-1] + cand = plano.topk(min(teto, plano.numel())).indices + penal = torch.zeros_like(plano) + n_vistos = 0 + for t in cand.tolist(): + chave = self._hash(ultimo, t) + visto = float(self.tab_bigramas[chave]) > 0.02 + if not visto: + penal[t] = -peso * 4.0 + else: + n_vistos += 1 + penal[t] = peso * float(self.tab_bigramas[chave]) + self.ultimo_score_coerencia = n_vistos / max(1, cand.numel()) + return (plano + penal).view(logits.shape) + + def telemetria(self) -> dict: + return {"coerencia_bigramas_vistos": self.ultimo_score_coerencia, + "estilo": [round(float(x), 3) + for x in torch.sigmoid(self.eixos).tolist()]} diff --git a/src/khtst/nlp/__init__.py b/src/khtst/nlp/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8f9fe8490e8ed268224da0536f79a75d53ca36de --- /dev/null +++ b/src/khtst/nlp/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +"""Pacote NLP (processamento) — v4. UnidadeNLP é cidadã de primeira classe: +enriquece o estado oculto com traços morfo-sintáticos, intenção e spans.""" +from khtst.nlp.unidade_nlp import UnidadeNLP + +__all__ = ["UnidadeNLP"] diff --git a/src/khtst/nlp/unidade_nlp.py b/src/khtst/nlp/unidade_nlp.py new file mode 100644 index 0000000000000000000000000000000000000000..c19c6db0e979d09aa68ea65316e745de7df0e1f6 --- /dev/null +++ b/src/khtst/nlp/unidade_nlp.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +"""UnidadeNLP — especialista de PROCESSAMENTO de linguagem (doc 15 §1) — v4. + +Traços (i) morfo-sintáticos leves via conv de n-gramas de caracteres, +(ii) intenção/tarefa por cabeça própria (10 classes), +(iii) spans de entidade por apontador (pontuações por posição). + +Enriquecimento ADITIVO com porta g=σ(W_g h): h⁺ = h + g ⊙ Δ(h) — nunca +substitui o fluxo do tronco (Teorema 15.1: não-interferência; inicialização +com última camada zerada ⇒ Δ=0 no acoplamento). +""" +from __future__ import annotations + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class RMSNormLocal(nn.Module): + def __init__(self, d: int, eps: float = 1e-6): + super().__init__() + self.peso = nn.Parameter(torch.ones(d)) + self.eps = eps + + def forward(self, x: torch.Tensor) -> torch.Tensor: + norma = x.float().pow(2).mean(-1, keepdim=True).add(self.eps).rsqrt() + return (x.float() * norma).to(x.dtype) * self.peso + + +class UnidadeNLP(nn.Module): + # 9 tarefas canônicas + "nlp-outro" + INTENCOES = ("lm", "noticia", "pontuacao", "instrucao", "tts", + "vqa", "ocr", "imagem_caption", "asr", "nlp-outro") + + def __init__(self, d: int = 192, n_intencoes: int = 10, dropout: float = 0.0): + super().__init__() + self.d = d + # (i) traços morfo-sintáticos: conv 1D sobre embeddings (char n-gram proxy) + self.conv_ngram = nn.Conv1d(d, d, kernel_size=3, padding=1, groups=4) + self.conv_ngram2 = nn.Conv1d(d, d, kernel_size=5, padding=2, groups=4) + self.norm_tracos = RMSNormLocal(d) + # (ii) cabeça de intenção + self.cabeca_intencao = nn.Linear(d, n_intencoes) + # (iii) apontador de entidades (score por posição) + self.pontuador = nn.Linear(d, 1) + # fusão aditiva com porta (Teorema 15.1) + self.W_delta = nn.Linear(3 * d, d, bias=False) + nn.init.zeros_(self.W_delta.weight) # Δ=0 no nascimento + self.W_gate = nn.Linear(d, d) + nn.init.constant_(self.W_gate.bias, -2.0) # g ≈ 0.12 no nascimento + self.dropout = nn.Dropout(dropout) + # telemetria + self.gate_medio = 0.0 + self.ultima_intencao: int | None = None + + def forward(self, h: torch.Tensor, mascara: torch.Tensor | None = None) \ + -> tuple[torch.Tensor, dict]: + """h: (B, T, d) estado oculto. Retorna (h⁺, info). + h⁺ = h + g ⊙ Δ([tracos; intencao_emb; entidade]) — Teorema 15.1.""" + B, T, d = h.shape + # (i) traços morfo-sintáticos locais + x = h.transpose(1, 2) # (B, d, T) + tracos = self.norm_tracos( + (self.conv_ngram(x) + self.conv_ngram2(x)).transpose(1, 2)) # (B,T,d) + # (ii) intenção — distribuição soft (diferenciável) projetada ao espaço d + logits_int = self.cabeca_intencao(h.mean(dim=1)) # (B, 10) + intencao_soft = torch.softmax(logits_int, dim=-1) # (B, 10) + emb_int = intencao_soft @ self.matriz_intencao(d) # (B, d) + emb_int = emb_int.unsqueeze(1).expand(B, T, d) + # (iii) spans de entidade — scores por posição, normalizados por seq + scores_ent = self.pontuador(h).squeeze(-1) # (B, T) + if mascara is not None: + scores_ent = scores_ent.masked_fill(~mascara.bool(), -1e4) + pesos_ent = torch.softmax(scores_ent, dim=-1) # (B, T) + entidade = (pesos_ent.unsqueeze(-1) * h).sum(1, keepdim=True) # (B,1,d) + entidade = entidade.expand(B, T, d) + # fusão aditiva com porta + delta = self.W_delta(self.dropout( + torch.cat([tracos, emb_int, entidade], dim=-1))) # (B, T, d) + gate = torch.sigmoid(self.W_gate(h)) # (B, T, d) + h_mais = h + gate * delta + # telemetria + self.gate_medio = float(gate.mean().detach()) + self.ultima_intencao = int(logits_int.argmax(dim=-1)[0].item()) + info = {"gate_nlp": self.gate_medio, + "intencao": self.INTENCOES[self.ultima_intencao] + if self.ultima_intencao is not None else "n/a", + "entropia_intencao": float( + -(intencao_soft.detach() + * (intencao_soft.detach() + 1e-9).log()).sum(-1).mean())} + return h_mais, info + + def matriz_intencao(self, d: int) -> torch.Tensor: + """Projeção (10, d) determinística das intenções ao espaço do modelo — + hash estável (sinusoidal) sem parâmetros extras.""" + if not hasattr(self, "_matriz_int") or self._matriz_int.shape != (10, d): + pos = torch.arange(10).unsqueeze(1).float() + div = torch.exp(torch.arange(0, d, 2).float() + * (-torch.log(torch.tensor(10000.0)) / d)) + pe = torch.zeros(10, d) + pe[:, 0::2] = torch.sin(pos * div) + pe[:, 1::2] = torch.cos(pos * div) + self._matriz_int = pe + return self._matriz_int + + def perda_intencao(self, logits_int: torch.Tensor, rotulo: torch.Tensor) \ + -> torch.Tensor: + return F.cross_entropy(logits_int, rotulo, ignore_index=-100) diff --git a/src/khtst/nucleo/__init__.py b/src/khtst/nucleo/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/khtst/nucleo/modelo.py b/src/khtst/nucleo/modelo.py new file mode 100644 index 0000000000000000000000000000000000000000..bc266bdbdd277dfac50baeb120c8085aa607b21f --- /dev/null +++ b/src/khtst/nucleo/modelo.py @@ -0,0 +1,574 @@ +# -*- coding: utf-8 -*- +"""KHTSTModel v2 — modelo unificado multimodal. + +Componentes (fundamentados em docs/matematica/00–10): + • decoder causal com MixtureOfAttention (global+janela+linear) e RoPE; + • LM head empatado (tied) — menos parâmetros (RAM) e regularização embutida; + • MoE AGRUPÁVEL com foco na tarefa nos últimos blocos (doc 10 §1) — fase + densa (todos os experts, máx conexões) → fase foco (top-k, ignora por + indexação o irrelevante); + • MTP com α aprendíveis sobre a tabela empatada (doc 10 §3); + • encoders multimodais completos (imagem/áudio/vídeo) + fusão por + cross-attention com gating por modalidade (prefixo no decoder); + • projetor cooperativo alinhado à memória SOM (perda auxiliar); + • geração autoregressiva com KV-cache, punição DINÂMICA de repetição + (eq. 10.8/10.9) e top-p; + • W8A8 opcional (QAT-STE) na lm_head (doc 08). +""" +from __future__ import annotations + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from khtst.acelerado import contagens_entropia as _cont_acelerada +from khtst.percepcao.atencao_moe import RefinamentoEntreMoEs +from khtst.percepcao.atencao import MixtureOfAttention +from khtst.percepcao.audio import CodificadorAudio +from khtst.percepcao.blocos import Bloco, BlocoV3, RMSNorm +from khtst.percepcao.escalacao import EscaladorComputo, PerfilComputo +from khtst.percepcao.fusao import FusaoMultimodal +from khtst.percepcao.imagem import CodificadorImagem +from khtst.percepcao.microunidades import BiGRUNaoCausal +from khtst.percepcao.moe import GRUPOS_TAREFA, MoEAgrupavel +from khtst.percepcao.ortogonais import CamadaCooperativa, CamadaOrtogonal +from khtst.percepcao.video import CodificadorVideo +from khtst.nlp.unidade_nlp import UnidadeNLP +from khtst.nlg.unidade_nlg import UnidadeNLG +from khtst.memoria.janela_1m import JanelaContexto1M +from khtst.quanta.quantizacao import QATW8A8 +from khtst.treino.mtp import MTPKHTST + + +class KHTSTModel(nn.Module): + # ordem canônica das tarefas (tabela de viés da MoE) + TAREFAS = ("lm", "noticia", "pontuacao", "instrucao", "tts", + "vqa", "ocr", "imagem_caption", "asr") + + def __init__(self, cfg, usar_multimodal: bool = True): + super().__init__() + m = cfg.modelo + self.cfg = cfg + self.d = m.d_modelo + self.emb = nn.Embedding(m.vocab, m.d_modelo) + nn.init.normal_(self.emb.weight, mean=0.0, std=0.02) # logits ~ N(0, 0.02²d) + # MoE agrupável: nos ÚLTIMOS n_camadas_moe blocos (capacidade abstrata + # no topo da pilha; blocos iniciais ficam densos — custo controlado) + cfg_moe = dict(getattr(m, "moe", {}) or {}) + self.usar_moe = bool(cfg_moe.get("ativo", True)) + self.moe_camadas: set[int] = set() + if self.usar_moe: + n_moe = int(cfg_moe.get("n_camadas_moe", 2)) + self.moe_camadas = set(range(max(0, m.n_camadas - n_moe), m.n_camadas)) + def _moe_para(indice: int): + if indice not in self.moe_camadas: + return None + return MoEAgrupavel(m.d_modelo, + n_grupos=int(cfg_moe.get("n_grupos", 3)), + experts_por_grupo=int(cfg_moe.get("experts_por_grupo", 2)), + d_ff_expert=int(cfg_moe.get("d_ff_expert", 160)), + top_k=int(cfg_moe.get("top_k", 2)), + n_tarefas=int(cfg_moe.get("n_tarefas", 9)), + margem_foco=float(cfg_moe.get("margem_foco", 1.0))) + # v3: tronco causal com microunidades (doc 11 §12) — BlocoV3 + cfg_micra = dict(getattr(m, "microunidades", {}) or {}) + # v8 (item g): MoE ENCODER-DECODER (2 enc + 4 dec) fora de ordem na + # ÚLTIMA camada MoE — capacidade abstrata máxima no topo da pilha; + # nasce neutra: roteador None ⇒ sem viés (Teorema 18.1) + cfg_ed = dict(getattr(m, "moe_enc_dec", {}) or {}) + self.moe_ed_camada: int | None = None + if self.usar_moe and bool(cfg_ed.get("ativo", True)) and self.moe_camadas: + self.moe_ed_camada = max(self.moe_camadas) + + def _moe_para(indice: int): + if indice not in self.moe_camadas: + return None + if indice == self.moe_ed_camada: + return self._moe_ed_factory() + return MoEAgrupavel(m.d_modelo, + n_grupos=int(cfg_moe.get("n_grupos", 3)), + experts_por_grupo=int(cfg_moe.get("experts_por_grupo", 2)), + d_ff_expert=int(cfg_moe.get("d_ff_expert", 160)), + top_k=int(cfg_moe.get("top_k", 2)), + n_tarefas=int(cfg_moe.get("n_tarefas", 9)), + margem_foco=float(cfg_moe.get("margem_foco", 1.0)), + vetorial=bool(cfg_moe.get("vetorial", True))) + self.blocos = nn.ModuleList([ + BlocoV3(m.d_modelo, m.n_cabecas, m.d_ff, m.janela_sliding, m.dropout, + moe=_moe_para(i), cfg_micra=cfg_micra) + for i in range(m.n_camadas)]) + # v5 (doc 16 §§1–2): atenção ENTRE camadas MoE consecutivas + retro- + # alimentação de rotas — nasce neutra (Teorema 16.1c), nunca regride + cfg_atn_moe = dict(getattr(m, "atencao_moe", {}) or {}) + if self.usar_moe and bool(cfg_atn_moe.get("ativo", True)): + camadas = sorted(self.moe_camadas) + for a, b in zip(camadas, camadas[1:]): + moe_a, moe_b = self.blocos[a].moe, self.blocos[b].moe + if moe_a is not None and moe_b is not None: + moe_b.moe_anterior = moe_a + moe_b.refino = RefinamentoEntreMoEs( + m.d_modelo, + alpha0=float(cfg_atn_moe.get("alpha0", 0.0))) + # confiança corrente (h_t do AgenteConfiança — definida pelo treino) + self.confianca_corrente: float = 0.5 + # v6 (doc 18 §1): ROTEADOR S-SOM compartilhado pelas camadas MoE — + # roteamento por S-SOM com nascimento neutro (Teorema 18.1) + cfg_rot = dict(getattr(m, "roteador", {}) or {}) + self.roteador_ssom = None + if self.usar_moe and bool(cfg_rot.get("ativo", True)): + from khtst.percepcao.roteador_ssom import RoteadorSSOM + self.roteador_ssom = RoteadorSSOM( + m.d_modelo, + n_tarefas=int(cfg_rot.get("n_tarefas", len(self.TAREFAS))), + k=int(cfg_rot.get("k", 24)), + lambda_rota=float(cfg_rot.get("lambda_rota", 0.35)), + gamma_empate=float(cfg_rot.get("gamma_empate", 0.15)), + conf_min=float(cfg_rot.get("conf_min", 0.25)), + min_amostras=int(cfg_rot.get("min_amostras", 200))) + for bloco in self.blocos: + if bloco.moe is not None: + bloco.moe.roteador = self.roteador_ssom + self.norm_f = RMSNorm(m.d_modelo) + self.lm_head = nn.Linear(m.d_modelo, m.vocab, bias=False) + self.lm_head.weight = self.emb.weight # tied (RAM otimizada) + # MTP (doc 10 §3): α aprendíveis, logits pela tabela empatada + cfg_mtp = dict(getattr(m, "mtp", {}) or {}) + self.usar_mtp = bool(cfg_mtp.get("ativo", True)) + self.mtp = MTPKHTST(m.d_modelo, k_cabecas=int(cfg_mtp.get("k_cabecas", 2)), + beta_entropia=float(cfg_mtp.get("beta_entropia", 0.01))) \ + if self.usar_mtp else None + if self.mtp is not None: + self.mtp.definir_peso_emb(self.emb.weight) # v4: rascunho empático + # v4 — NLP (processar) e NLG (gerar) como unidades de primeira classe: + cfg_nlp = dict(getattr(m, "nlp", {}) or {}) + self.nlp = UnidadeNLP(m.d_modelo, n_intencoes=int(cfg_nlp.get("n_intencoes", 10))) \ + if bool(cfg_nlp.get("ativo", True)) else None + cfg_nlg = dict(getattr(m, "nlg", {}) or {}) + self.nlg = UnidadeNLG(m.d_modelo, n_estilos=int(cfg_nlg.get("n_estilos", 8))) \ + if bool(cfg_nlg.get("ativo", True)) else None + # v4 — janela de contexto 1M com compressão INDEXADA (doc 13) + cfg_j1 = getattr(cfg, "janela_1m", None) + if cfg_j1 is not None and bool(getattr(cfg_j1, "ativo", True)): + self.janela_1m = JanelaContexto1M( + m.d_modelo, m_max=int(getattr(cfg_j1, "m_max", 7813))) + else: + self.janela_1m = None + # v4 — auto-escala por computo (doc 12): perfil + escalador + cfg_esc = dict(getattr(m, "escalacao", {}) or {}) + self.perfil_computo = PerfilComputo() + self.escalador = EscaladorComputo( + d0=int(cfg_esc.get("d0", 64)), + n_max=int(cfg_esc.get("n_max_ramos", 6)), + k_max=int(cfg_esc.get("k_rec_max", 3)), + alpha=float(cfg_esc.get("alpha", 0.5))) \ + if bool(cfg_esc.get("ativo", True)) else None + self.info_escalacao: dict = {} + # multimodal completo-pesado (escala via config): + self.usar_multimodal = usar_multimodal + if usar_multimodal: + mi, ma, mv = m.imagem, m.audio, m.video + self.enc_imagem = CodificadorImagem(mi["tam_patch"], mi["resolucao"], mi["d"], + mi["n_camadas"], mi["n_cabecas"], d_saida=self.d) + self.enc_audio = CodificadorAudio(ma["n_mels"], ma["d"], ma["n_camadas"], + ma["n_cabecas"], d_saida=self.d) + self.enc_video = CodificadorVideo(mv["n_frames"], mv["tam_patch"], mv["resolucao"], + mv["d"], mv["n_camadas"], d_saida=self.d) + self.fusao = FusaoMultimodal(self.d) + # projetor cooperativo p/ alinhamento com memória SOM (eq. 9.2) + self.projetor = CamadaCooperativa(m.d_modelo, n_expertos=m.n_experts_fusao) + self.alinhamento = CamadaOrtogonal(m.d_modelo, m.d_modelo, vies=False) + self.ultima_perda: float | None = None + self.perda_balanceamento: float = 0.0 + self._coletar: dict = {} + self.qat = False + self._qat = QATW8A8(por_canal=True) # v5: QAT com STE (sem fakes) + + # ---------------- fases MoE (doc 10 §1.1) ---------------- + def _moe_ed_factory(self): + """Fábrica da MoE encoder-decoder (item g) — usada por _moe_para.""" + from khtst.percepcao.moe import MoEEncoderDecoderKHTST + cfg_ed = dict(getattr(self.cfg.modelo, "moe_enc_dec", {}) or {}) + m = self.cfg.modelo + moe_ed = MoEEncoderDecoderKHTST( + m.d_modelo, + n_encoders=int(cfg_ed.get("n_encoders", 2)), + n_decoders=int(cfg_ed.get("n_decoders", 4)), + d_ff_expert=int(cfg_ed.get("d_ff_expert", 160)), + top_k=int(cfg_ed.get("top_k", 2)), + fora_de_ordem=bool(cfg_ed.get("fora_de_ordem", True))) + moe_ed._tipo_enc_dec = True + return moe_ed + + def definir_fase_moe(self, fase: str): + """'densa' (todos os experts — máx conexões) ou 'foco' (top-k + máscara). + v8: a MoE enc-dec (item g) alterna AGRUPADA (densa) / DESAGRUPADA (foco) + — agrupável/desagrupável em runtime (Teorema 19.13).""" + for bloco in self.blocos: + if bloco.moe is None: + continue + if getattr(bloco.moe, "_tipo_enc_dec", False): + # v8.1 — fase densa = softmax TOTAL (2 enc + 4 dec); foco = top-k. + # agrupar/desagrupar permanece capacidade INDEPENDENTE (Teo 19.13) + bloco.moe.fase_densa = (fase == "densa") + continue + bloco.moe.fase_densa = (fase == "densa") + + @property + def compositores(self) -> list: + """Compositores de microunidades (para o GestorCrescimento — doc 11 §5).""" + return [b.composto for b in self.blocos if hasattr(b, "composto")] + + def registrar_confianca(self, h_t: float): + """Distribui h_t (AgenteConfiança) aos compositores — temperatura do + gating (menor confiança ⇒ softmax mais plano ⇒ mais exploração).""" + self.confianca_corrente = float(min(max(h_t, 0.0), 1.0)) + for b in self.blocos: + if hasattr(b, "registrar_confianca"): + b.registrar_confianca(self.confianca_corrente) + + def indice_tarefa(self, tarefa: str | None) -> int: + return self.TAREFAS.index(tarefa) if tarefa in self.TAREFAS else -1 + + # ---------------- passagem principal ---------------- + def forward(self, ids: torch.Tensor, alvo: torch.Tensor | None = None, + emb_prefixo: torch.Tensor | None = None, coletar: dict | None = None, + tarefa: str | None = None): + """ids: (B,T). alvo: (B,T) com -100 nas posições sem perda (padrão LM shift). + emb_prefixo: (B,1,d) do fusionador multimodal (prefix-tuning). + v4: a UnidadeNLP enriquece o estado oculto ANTES da normalização final + (aditiva com porta — Teorema 15.1: nasce neutra, nunca regride).""" + self.ultima_perda = None + x = self.emb(ids) + if emb_prefixo is not None: + x = torch.cat([emb_prefixo.unsqueeze(1), x], dim=1) + co = coletar if coletar is not None else self._coletar + for bloco in self.blocos: + bloco._tarefa_corrente = tarefa + x, _, _ = bloco(x, coletar=co, tarefa=tarefa) + self.ultimo_info_nlp = {} + if self.nlp is not None: + x, self.ultimo_info_nlp = self.nlp(x) # h⁺ = h + g⊙Δ (doc 15 §1) + x = self.norm_f(x) + self._ultimo_oculto = x # para MTP sem re-executar o trunk (v2) + if self.qat: + wq = self._qat.quantiza_peso(self.lm_head.weight) # STE (Teorema 8.3) + logits = F.linear(x, wq) + else: + logits = self.lm_head(x) + if alvo is not None: + # Convenção: alvo[t] = rótulo do token previsto APÓS ver t tokens + # (logits[j] prevê o token em j+1 ⇒ CE(logits[:,:-1], alvo[:,1:])). + if emb_prefixo is not None: + perda = F.cross_entropy(logits[:, :-1].reshape(-1, logits.shape[-1]), + alvo.reshape(-1), ignore_index=-100) + else: + perda = F.cross_entropy(logits[:, :-1].reshape(-1, logits.shape[-1]), + alvo[:, 1:].reshape(-1), ignore_index=-100) + self.ultima_perda = float(perda.detach()) + pb = self.projetor.perda_balanceamento + self.perda_balanceamento = float(pb) if not isinstance(pb, float) else pb + return logits, perda + return logits, None + + # ---------------- MTP (doc 10 §3) ---------------- + def mtp_do_trunk(self, ids: torch.Tensor, alvo: torch.Tensor, + tarefa: str | None = None, + com_prefixo: bool = False, + h_conf: torch.Tensor | None = None) -> torch.Tensor | None: + """MTP sobre o estado oculto do MESMO forward (grafo compartilhado — + PCGrad separa os gradientes depois, doc 10 §5). v3: `h_conf` (B,T) ativa + a máscara de K adaptativo (doc 11 Teorema 11.6). `com_prefixo`: descarta + a posição do prefixo para alinhar MTP ao eixo temporal dos ids.""" + if self.mtp is None: + return None + h = getattr(self, "_ultimo_oculto", None) + T_esperado = ids.shape[1] + (1 if com_prefixo else 0) + if h is None or h.shape[0] != ids.shape[0] or h.shape[1] != T_esperado \ + or not h.requires_grad: + x = self.emb(ids) + for bloco in self.blocos: + bloco._tarefa_corrente = tarefa + x, _, _ = bloco(x, tarefa=tarefa) + h = self.norm_f(x) + if com_prefixo and h.shape[1] == ids.shape[1] + 1: + h = h[:, 1:] + return self.mtp.perda(h, self.emb.weight, alvo, h_conf=h_conf) + + # ---------------- multimodal ---------------- + @torch.no_grad() + def codificar_multimodal(self, entradas: dict) -> torch.Tensor | None: + """entradas: {'imagem': (B,3,H,W), 'audio': (B,N), 'video': (B,T,3,H,W), + 'texto_emb': (B,d)} → prefixo (B, d) pronto para fusão.""" + if not self.usar_multimodal: + return None + emb = {} + if "imagem" in entradas: + emb["imagem"] = self.enc_imagem(entradas["imagem"]) + if "audio" in entradas: + emb["audio"] = self.enc_audio(entradas["audio"]) + if "video" in entradas: + emb["video"] = self.enc_video(entradas["video"]) + if "texto_emb" in entradas: + emb["texto"] = entradas["texto_emb"] + if not emb: + return None + vetor, gates = self.fusao(emb) + self.ultimos_gates = gates + return vetor + + # ---------------- memória SOM ---------------- + @torch.no_grad() + def contexto(self, ids: torch.Tensor) -> torch.Tensor: + """Embedding médio do estado oculto — entrada da memória auto-organizável.""" + x = self.emb(ids) + for bloco in self.blocos: + x, _, _ = bloco(x) + x = self.norm_f(x) + return x.mean(dim=1) + + def alinhar_som(self, ids: torch.Tensor, orquestrador=None, tarefa: str | None = None, + usar_oculto: bool = True): + """Perda auxiliar: projeta o estado oculto (COM gradiente) ao espaço da + memória; protótipos são por amostra e stop-grad (SOM não treina aqui; + Teorema 10.5 — não-regressão da consolidação). Reusa o estado oculto do + forward recente quando compatível (v2 — evita 3º passe pelo trunk).""" + h = getattr(self, "_ultimo_oculto", None) if usar_oculto else None + if h is None or h.shape[0] != ids.shape[0] or h.shape[1] != ids.shape[1] \ + or not h.requires_grad: + x = self.emb(ids) + for bloco in self.blocos: + bloco._tarefa_corrente = tarefa + x, _, _ = bloco(x, tarefa=tarefa) + h = self.norm_f(x) + ctx = h.mean(dim=1) # (B, d) com gradiente + proj = self.alinhamento(ctx) + if orquestrador is not None: + # v5 — recall atencional sobre TODAS as variantes (doc 16 §§3–4): + # ẑ ∈ casco{∪_v W_v}; bônus de novidade protege neurônios pouco usados + if hasattr(orquestrador, "mapear_atencao"): + alvo = orquestrador.mapear_atencao(ctx.detach()) + else: + alvo = orquestrador.mapear(ctx.detach()) + return F.mse_loss(proj, alvo) + return F.mse_loss(proj, ctx.detach()) # fallback: identidade regularizada + + # ---------------- geração ---------------- + @torch.no_grad() + def gerar(self, ids_prompt: torch.Tensor, max_novos: int = 48, temperatura: float = 0.8, + top_k: int = 20, top_p: float | None = 0.9, kv_max: int | None = None, + punicao_repeticao: float = 1.2, h_max: float = 10.0, + tarefa: str | None = None, + emb_prefixo: torch.Tensor | None = None) -> list[int]: + """Geração com KV-cache, punição DINÂMICA de repetição (doc 10 §8: + pen = 1+(base−1)·1[repetido]·min(1, H/H_max)) e filtro top-p. + Operação por amostra única (KV-cache do decode é B=1). + v6 — emb_prefixo: posições multimodais pré-agregadas (1, P, d) que + abrem o contexto (VQA/ocr); None ⇒ v5 EXATO (nascimento neutro).""" + kv_max = kv_max or self.cfg.modelo.kv_cache_max + ids_prompt = ids_prompt[:1] # amostra única + for bloco in self.blocos: + bloco.reset_estado() + caches = [(None, None)] * len(self.blocos) + x = self.emb(ids_prompt) # estados ocultos, não ids! + contextos: list[int] = [int(t) for t in ids_prompt[0]] + if emb_prefixo is not None: + pref = emb_prefixo.to(x.dtype) + if pref.dim() == 2: + pref = pref.unsqueeze(1) + n_pref = pref.shape[1] + x = torch.cat([pref, x], dim=1) + contextos = [0] * n_pref + contextos # reservas não-geradas + novos: list[int] = [] + V = self.cfg.modelo.vocab + for passo in range(max_novos): + co = {} + for li, bloco in enumerate(self.blocos): + ck, cv = caches[li] if caches[li] != (None, None) else (None, None) + x, k, v = bloco(x, cache_k=ck, cache_v=cv, kv_max=kv_max, coletar=co, + tarefa=tarefa) + caches[li] = (k, v) + x = self.norm_f(x) + # v4 — NLG modula o estado oculto (doc 15 §1: estilo aditivo) + h_dec = self.nlg.condicionar(x) if self.nlg is not None else x + logits = self.lm_head(h_dec[:, -1]) + logits = logits / max(temperatura, 1e-3) + # punição dinâmica de repetição (eq. 10.8/10.9) — v8: núcleo C + # (Teorema 19.25) com fallback torch de semântica idêntica + _, H_ctx, fator_ent = _cont_acelerada(contextos, V, h_max, 1.0) + contagens = torch.bincount(torch.tensor(contextos, dtype=torch.long), + minlength=V) + repetidos = contagens > 0 + pen = 1.0 + (punicao_repeticao - 1.0) * fator_ent * repetidos.float() + logits = logits / pen.unsqueeze(0) + # v4 — re-ranking de coerência da NLG (doc 15 §1.ii): penaliza + # bigramas inéditos no contexto recente (dinâmico, O(|V_cand|)) + if self.nlg is not None and len(contextos) >= 2: + peso_rr = float(dict(getattr(self.cfg.modelo, "nlg", {}) or {}) + .get("peso_rerank", 0.35)) + logits = self.nlg.rerank_coerencia(logits, contextos, + peso=peso_rr) + # top-k + if top_k and top_k < V: + topo = logits.topk(top_k, dim=-1) + logits_f = torch.full_like(logits, float("-inf")) + logits_f.scatter_(-1, topo.indices, topo.values) + logits = logits_f + # top-p (núcleo) — sempre mantém o 1º (evita conjunto vazio) + if top_p is not None and 0.0 < top_p < 1.0: + ordenados, idx = torch.sort(logits, descending=True, dim=-1) + probs = torch.softmax(ordenados, dim=-1) + acum = probs.cumsum(dim=-1) + manter = (acum - probs) < top_p # shift: 1º sempre entra + ordenados = torch.where(manter, ordenados, + torch.full_like(ordenados, float("-inf"))) + logits = torch.full_like(logits, float("-inf")).scatter_(-1, idx, ordenados) + probs = torch.softmax(logits[0], dim=-1) + if not torch.isfinite(probs).all() or float(probs.sum()) <= 0: + idx = torch.argmax(logits[0]) + else: + idx = torch.multinomial(probs, 1) + t = int(idx) + if t == 2: # + break + novos.append(t) + contextos.append(t) + x = self.emb(torch.tensor([[t]])) + return novos + + # ---------------- v4: janela 1M, auto-escala e telemetria ---------------- + @torch.no_grad() + def memorizar_janela_1m(self, ids: torch.Tensor): + """Insere o estado oculto corrente na janela indexada 1M (doc 13 §4).""" + if self.janela_1m is None or ids.shape[1] < 32: + return + x = self.emb(ids) + for bloco in self.blocos: + x, _, _ = bloco(x) + self.janela_1m.insere(self.norm_f(x).squeeze(0)) + + @torch.no_grad() + def recuperar_janela_1m(self, q: torch.Tensor, k: int = 4) -> torch.Tensor | None: + """Consulta top-k resumos (doc 13 §2). q: (d,) estado oculto consulta.""" + if self.janela_1m is None or self.janela_1m.n_segmentos == 0: + return None + return self.janela_1m.consulta(q, k=k) + + def reavaliar_computo(self) -> dict: + """Auto-escala (doc 12 eq. 12.2): lê o perfil C e recalcula a capacidade + alvo. Monotonia garantida (Teorema 12.1a) — computo extra nunca reduz + a capacidade; a expansão real do compositor fica a cargo do + GestorCrescimento (doc 11 §5) com herança segura (Teorema 12.2).""" + if self.escalador is None: + return {} + C = self.perfil_computo.computo() + alvo = self.escalador.escalar(C) + self.info_escalacao = {"computo_C": round(C, 3), "nucleos": self.perfil_computo.n_nucleos, + "ram_livre_mb": round(self.perfil_computo.ram_livre_mb(), 1), + **alvo} + return self.info_escalacao + + def registrar_passo_computo(self, dt_s: float): + if self.perfil_computo is not None: + self.perfil_computo.registrar_passo(dt_s) + + def gerar_especulativo(self, ids_prompt: list[int], max_novos: int = 48, + **kwargs): + """Geração com decodificação especulativa em árvore (doc 14) — + inocuidade comprovada (Teorema 14.1). Requer MTP ativo.""" + from khtst.nlg.decodificador_especulativo import DecodificadorEspeculativoMedusa + if self.mtp is None: + raise RuntimeError("MTP inativo — sem rascunho para especulação") + dec = DecodificadorEspeculativoMedusa(self, self.mtp) + return dec.gerar(ids_prompt, max_novos=max_novos, **kwargs) + + # ---------------- telemetria auxiliares ---------------- + def metricas_estruturais(self) -> dict: + out = {"ortogonalidade": self.alinhamento.metrica_ortogonalidade(), + "balanceamento_moe": float(getattr(self.projetor, "ultimo_f", torch.zeros(2)).mean())} + if self._coletar.get("gate_atencao"): + g = self._coletar["gate_atencao"] + out["gate_global"], out["gate_janela"], out["gate_linear"] = g + # MoE agrupável: uso e fase + for i, bloco in enumerate(self.blocos): + moe = getattr(bloco, "moe", None) + if moe is None: + continue + # v8 (item g) — MoE enc-dec: telemetria própria + if getattr(moe, "_tipo_enc_dec", False): + out[f"moe{i}/enc_uso_medio"] = float(moe.uso_enc_ema.mean()) + out[f"moe{i}/dec_uso_medio"] = float(moe.uso_dec_ema.mean()) + out[f"moe{i}/dec_uso_min"] = float(moe.uso_dec_ema.min()) + out[f"moe{i}/agrupado"] = 1.0 if moe.agrupado else 0.0 + out[f"moe{i}/perda_lb"] = float(moe.perda_lb) + continue + if moe.uso_ema is not None: + out[f"moe{i}/experts_ativos"] = float((moe.uso_ema > 1e-3).sum()) + out[f"moe{i}/uso_max"] = float(moe.uso_ema.max()) + out[f"moe{i}/perda_lb"] = float(moe.perda_lb) + out[f"moe{i}/perda_ort"] = float(moe.perda_ort) + out[f"moe{i}/fase"] = 1.0 if moe.fase_densa else 0.0 + # v5 — atenção entre MoEs (doc 16 §2) + out[f"moe{i}/beta_feedback"] = float(moe.beta_feedback) + out[f"moe{i}/entropia_atn"] = moe.ultima_entropia_atn + if moe.refino is not None: + out[f"moe{i}/alpha_refino"] = float(moe.refino.alpha) + if self.mtp is not None and self.mtp.ultimos: + for j, a in enumerate(self.mtp.ultimos.get("alphas", [])): + out[f"mtp/alpha{j}"] = a + if "termos_ce" in self.mtp.ultimos: + out["mtp/termos_ce"] = float(self.mtp.ultimos["termos_ce"]) + # microunidades v3 (doc 11): uso por ramo, gating e recursão + for i, bloco in enumerate(self.blocos): + comp = getattr(bloco, "composto", None) + if comp is None: + continue + out[f"micra{i}/n_ramos"] = len(comp.nome_ramos) + out[f"micra{i}/ramos_ativos"] = int((comp.ramo_ativo > 0.5).sum()) + out[f"micra{i}/passos_rec"] = comp.ultimo_passos_rec + if comp.ultimo_alpha is not None: + for r, a in enumerate(comp.ultimo_alpha.tolist()): + out[f"micra{i}/alpha{r}"] = a + if comp.uso_ema is not None: + for r, a in enumerate(comp.uso_ema.tolist()): + out[f"micra{i}/uso{r}"] = a + # v4 — NLP / NLG / janela 1M / auto-escala (doc 15 §3.7) + if self.nlp is not None: + out["nlp/gate_medio"] = self.nlp.gate_medio + if self.nlp.ultima_intencao is not None: + out["nlp/intencao_id"] = self.nlp.ultima_intencao + if self.nlg is not None: + t = self.nlg.telemetria() + out["nlg/coerencia_bigramas_vistos"] = t["coerencia_bigramas_vistos"] + # estilo: 8 eixos → telemetria só aceita escalares (média + dispersão) + est = t["estilo"] + out["nlg/estilo_medio"] = sum(est) / max(1, len(est)) + out["nlg/estilo_disp"] = (max(est) - min(est)) + if self.janela_1m is not None: + for k2, v2 in self.janela_1m.telemetria().items(): + out[f"janela1m/{k2}"] = v2 + if self.info_escalacao: + for k2, v2 in self.info_escalacao.items(): + out[f"computo/{k2}"] = v2 + if self.mtp is not None and hasattr(self.mtp, "_peso_emb") \ + and self.mtp._peso_emb is not None: + out["mtp/rascunho_pronto"] = 1.0 + # v6 — roteador S-SOM (doc 18 §1) + if self.roteador_ssom is not None: + st = self.roteador_ssom.estatisticas() + out["roteador/amostras"] = st["amostras"] + out["roteador/pronto"] = 1.0 if st["pronto"] else 0.0 + out["roteador/frac_rotas"] = st["frac_rotas_ativas"] + out["roteador/conf_media"] = st["conf_media"] + out["roteador/drift"] = st["drift_codebook"] + # telemetria hub só aceita escalares — filtra strings/listas/dicts + out = {k: v for k, v in out.items() + if isinstance(v, (int, float)) and v == v} + return out + + @torch.no_grad() + def rank_efetivo_oculto(self, ids: torch.Tensor) -> float: + from khtst.telemetria.hub import rank_efetivo + x = self.emb(ids) + for bloco in self.blocos: + x, _, _ = bloco(x) + return rank_efetivo(x.squeeze(0) if x.shape[0] == 1 else x[0]) diff --git a/src/khtst/percepcao/__init__.py b/src/khtst/percepcao/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/khtst/percepcao/atencao.py b/src/khtst/percepcao/atencao.py new file mode 100644 index 0000000000000000000000000000000000000000..a8ee9fe58274a4cd7a5b6e6d3538dd75947fb3d7 --- /dev/null +++ b/src/khtst/percepcao/atencao.py @@ -0,0 +1,327 @@ +# -*- coding: utf-8 -*- +"""Mecanismos de atenção combináveis (item 10) com KV-cache O(1)/token. + +Fundamentos: docs/matematica/07 e docs/matematica/19 (KHTST v8). Quatro +mecanismos por cabeça: + GLOBAL — softmax completo, O(n²d); + JANELA — janela deslizante de raio `raio`, O(n·r·d); + LINEAR — kernel φ(x)=elu(x)+1, O(n·m·d), estado recorrente p/ decodificação; + ARVORE — atenção em ÁRVORE BIDIRECIONAL FORA DE ORDEM (doc 19 §3): + permutação bit-reversal (ordem Euler da árvore binária completa) + + máscara de banda estendida por blocos de subárvore 2^k; + invariância por permutação garante igualdade funcional com a + atenção mascarada em ordem natural (Teorema 19.3). + +Gating softmax aprendido mistura GLOBAL+JANELA+LINEAR (custo esperado = +mistura convexa, Teorema 7.1); o mecanismo ÁRVORE entra por um peso escalar +APRENDÍVEL `peso_arvore` nascido em 0.0 — nascimento NEUTRO exato +(Teorema 19.4: forward idêntico ao v6 com peso_arvore=0; não-regressão). +KV-cache recorta por `kv_max` (otimização de RAM). + +KHTST v8: raio_janela e kv_max padrão ×4 (128/2048) — cobertura de contexto +multiplicada por 4 com custo O(n·r·d) ainda linear em n (Teorema 19.2). +""" +from __future__ import annotations + +import math + +import torch +import torch.nn as nn +import torch.nn.functional as F + +MECANISMOS = ("global", "janela", "linear", "arvore") + +# v8 — expansão ×4 (requisito): raio padrão da janela e recorte do KV-cache +RAIO_JANELA_PADRAO = 128 # era 32 (×4) +KV_MAX_PADRAO = 2048 # era 512 (×4) + + +def aplicar_rope(q: torch.Tensor, k: torch.Tensor, base: float = 10000.0) -> tuple[torch.Tensor, torch.Tensor]: + """RoPE padrão. q,k: (B, H, T, dk). Rotação relativa preserva produto interno + dependente apenas da diferença de posições (prova: e^{iθp} conjugação).""" + B, H, T, dk = q.shape + pos = torch.arange(T, device=q.device, dtype=torch.float32) + freqs = base ** (-torch.arange(0, dk, 2, device=q.device, dtype=torch.float32) / dk) + ang = torch.outer(pos, freqs) # (T, dk/2) + cos, sin = ang.cos(), ang.sin() + cos = cos[None, None, :, :] + sin = sin[None, None, :, :] + + def rot(x): + x1, x2 = x[..., 0::2], x[..., 1::2] + return torch.stack((x1 * cos - x2 * sin, x1 * sin + x2 * cos), dim=-1).flatten(-2) + + return rot(q), rot(k) + + +def permutacao_arvore(T: int, device: torch.device | None = None) -> torch.Tensor: + """Permutação fora-de-ordem σ (bit-reversal) sobre T posições — a ordem + de travessia folha-da-árvore-binária-completa (Euler). σ(T_next_pow2) + restrita a [0,T): positions σ[i] guarda a posição natural do i-ésimo + elemento na ordem árvore. Propriedade (Teorema 19.3): atenção é + equivariante a σ sobre chaves/valores — reordenar NÃO muda o resultado + funcional; a máscara é construída NO espaço permutado. + """ + Tp = 1 + while Tp < T: + Tp *= 2 + idx = torch.arange(Tp, device=device, dtype=torch.long) + bits = max(1, Tp.bit_length() - 1) + rev = torch.zeros(Tp, device=device, dtype=torch.long) + for b in range(bits): + rev |= ((idx >> b) & 1) << (bits - 1 - b) + sigma = rev[rev < T] + return sigma + + +class AtencaoCabecas(nn.Module): + """Atenção global multi-cabeça causal com suporte a KV-cache. + + v8 — renomeada de `AtencaoCabeças` (ASCII: identificadores sem cedilha; + o alias compatível permanece no fim do módulo). + """ + + def __init__(self, d: int, n_cabecas: int, dropout: float = 0.0): + super().__init__() + assert d % n_cabecas == 0 + self.h, self.dk = n_cabecas, d // n_cabecas + self.wq = nn.Linear(d, d, bias=False) + self.wk = nn.Linear(d, d, bias=False) + self.wv = nn.Linear(d, d, bias=False) + self.wo = nn.Linear(d, d, bias=False) + self.dropout = dropout + + def forward(self, x: torch.Tensor, cache_k: torch.Tensor | None = None, + cache_v: torch.Tensor | None = None, kv_max: int = KV_MAX_PADRAO): + B, T, D = x.shape + q = self.wq(x).view(B, T, self.h, self.dk).transpose(1, 2) + k = self.wk(x).view(B, T, self.h, self.dk).transpose(1, 2) + v = self.wv(x).view(B, T, self.h, self.dk).transpose(1, 2) + q, k = aplicar_rope(q, k) + if cache_k is not None: # decodificação: 1 token novo + k = torch.cat([cache_k, k], dim=2) + v = torch.cat([cache_v, v], dim=2) + if k.shape[2] > kv_max: # recorte por RAM + k, v = k[:, :, -kv_max:], v[:, :, -kv_max:] + Tkv = k.shape[2] + pont = (q @ k.transpose(-2, -1)) / math.sqrt(self.dk) # √d (Teorema 7.0) + if cache_k is None and T > 1: # máscara causal no treino + masc = torch.triu(torch.ones(T, Tkv, device=x.device, dtype=torch.bool), 1) + pont = pont.masked_fill(masc, float("-inf")) + y = F.softmax(pont, dim=-1) + y = F.dropout(y, self.dropout, self.training) + y = (y @ v).transpose(1, 2).reshape(B, T, D) + return self.wo(y), k, v + + +class AtencaoArvore(nn.Module): + """Atenção em ÁRVORE BIDIRECIONAL FORA DE ORDEM (doc 19 §3, KHTST v8). + + Construção: + • σ = permutação bit-reversal (ordem Euler de árvore binária completa); + • q̃ = q[:, :, σ]; k̃, ṽ idem (FORA DE ORDEM — computo em ordem árvore); + • máscara no espaço permutado: banda |i−j| ≤ r (vizinhos de folha) ∪ + blocos de subárvore: pares (i,j) com ⌊i/2^k⌋ = ⌊j/2^k⌋ para algum + k ≤ K_sub (irmãos de nível k — conexões LONGAS de árvore); + • BIDIRECIONAL: sem máscara causal (para encoders / quando `causal=False`); + CAUSAL (decoder): cada consulta i vê apenas j ≤ i no espaço permutado + (ordem Euler preserva prefixo de subárvore); + • saída restaurada por σ⁻¹ (inverse permutation). + + Custo: O(n·(2r+1+Σ_k 2^k)·d) = O(n·r·d) — linear em n (Teorema 19.2); + cobertura: todo par (t,s) com |t−s| ≤ r OU na mesma subárvore 2^k conecta + em ≤ 2 saltos mascarados (Teorema 19.1). + """ + + def __init__(self, d: int, n_cabecas: int, raio: int = RAIO_JANELA_PADRAO, + dropout: float = 0.0): + super().__init__() + assert d % n_cabecas == 0 + self.h, self.dk = n_cabecas, d // n_cabecas + self.raio = raio + self.dropout = dropout + self.wq = nn.Linear(d, d, bias=False) + self.wk = nn.Linear(d, d, bias=False) + self.wv = nn.Linear(d, d, bias=False) + self.wo = nn.Linear(d, d, bias=False) + self._sigma_cache: dict[int, torch.Tensor] = {} + + def _sigma(self, T: int, device: torch.device) -> torch.Tensor: + if T not in self._sigma_cache: + if len(self._sigma_cache) > 8: + self._sigma_cache.clear() # higiene de RAM (item h) + self._sigma_cache[T] = permutacao_arvore(T, device) + return self._sigma_cache[T] + + def _mascara_arvore(self, T: int, causal: bool, device: torch.device) -> torch.Tensor: + """Máscara (T,T) bool: True = BLOQUEADO. Banda r + blocos 2^k.""" + i = torch.arange(T, device=device).view(-1, 1) + j = torch.arange(T, device=device).view(1, -1) + banda = (i - j).abs() <= self.raio + # blocos de subárvore: mesmo pai no nível k (k = 1..log2(T)) + conecta = banda.clone() + k = 1 + while (1 << k) <= T and k <= 6: # até 2^6=64: custo da máscara O(T²) só em reconstrução bool + conecta |= (i // (1 << k)) == (j // (1 << k)) + k += 1 + masc = ~conecta + if causal: + masc = masc | (j > i) + return masc + + def forward(self, x: torch.Tensor, cache_k: torch.Tensor | None = None, + cache_v: torch.Tensor | None = None, kv_max: int = KV_MAX_PADRAO, + causal: bool = True): + B, T, D = x.shape + q = self.wq(x).view(B, T, self.h, self.dk).transpose(1, 2) + k = self.wk(x).view(B, T, self.h, self.dk).transpose(1, 2) + v = self.wv(x).view(B, T, self.h, self.dk).transpose(1, 2) + q, k = aplicar_rope(q, k) + if cache_k is not None: + k = torch.cat([cache_k, k], dim=2) + v = torch.cat([cache_v, v], dim=2) + if k.shape[2] > kv_max: + k, v = k[:, :, -kv_max:], v[:, :, -kv_max:] + Tkv = k.shape[2] + # FORA DE ORDEM: permuta consultas/chaves/valores pela ordem árvore + Tq = q.shape[2] + sig_q = self._sigma(Tq, q.device) + sig_k = self._sigma(Tkv, k.device) if Tkv == Tq else None + if Tq > 1 and sig_k is not None: + q_p = q[:, :, sig_q] + k_p = k[:, :, sig_k] + v_p = v[:, :, sig_k] + pont = (q_p @ k_p.transpose(-2, -1)) / math.sqrt(self.dk) + if cache_k is None: # máscara de árvore no treino/prefill + causal_q = causal and Tq > 1 and cache_k is None + masc = self._mascara_arvore(Tq, causal_q, x.device) + pont = pont.masked_fill(masc, float("-inf")) + y_p = F.softmax(pont, dim=-1) + y_p = F.dropout(y_p, self.dropout, self.training) + y_p = y_p @ v_p + # restaura ordem natural: y[σ[i]] = y_p[i] ⇒ y = y_p[inv] + inv = torch.empty_like(sig_q) + inv[sig_q] = torch.arange(Tq, device=x.device) + y = y_p[:, :, inv] + else: + # decodificação T=1: banda estendida no cache — últimos 2r+1 + + # amostragem log-estruturada (nós ancestrais 2^k) — sem permutação + pont = (q @ k.transpose(-2, -1)) / math.sqrt(self.dk) + Tkv_c = pont.shape[-1] + if Tkv_c > (2 * self.raio + 1 + 8): + idx_r = torch.arange(Tkv_c - (2 * self.raio + 1), Tkv_c, + device=x.device) + ancestrais = [Tkv_c - (2 * self.raio + 1) - (1 << kk) + for kk in range(0, 10) + if Tkv_c - (2 * self.raio + 1) - (1 << kk) >= 0] + idx_a = torch.tensor(sorted(set(ancestrais)), device=x.device) + idx = torch.cat([idx_a, idx_r]) + pont = pont.index_select(-1, idx) + v_sel = v.index_select(2, idx) + else: + v_sel = v + y = F.softmax(pont, dim=-1) + y = y @ v_sel + y = y.transpose(1, 2).reshape(B, T, D) + return self.wo(y), k, v + + +class AtencaoLinear(nn.Module): + """Atenção linear (Katharopoulos et al., 2020) — associatividade prova o + custo O(n·m·d): φ(Q)(φ(K)ᵀV). Estado `S` permite decodificação O(1)/token.""" + + def __init__(self, d: int, n_cabecas: int): + super().__init__() + self.h, self.dk = n_cabecas, d // n_cabecas + self.wq = nn.Linear(d, d, bias=False) + self.wk = nn.Linear(d, d, bias=False) + self.wv = nn.Linear(d, d, bias=False) + self.wo = nn.Linear(d, d, bias=False) + + @staticmethod + def _phi(x): + return F.elu(x) + 1.0 + + def forward(self, x: torch.Tensor, estado: torch.Tensor | None = None, **_): + B, T, D = x.shape + q = self._phi(self.wq(x)).view(B, T, self.h, self.dk).transpose(1, 2) + k = self._phi(self.wk(x)).view(B, T, self.h, self.dk).transpose(1, 2) + v = self.wv(x).view(B, T, self.h, self.dk).transpose(1, 2) + if estado is None: + # paralelo causal: S_t = Σ_{s≤t} φ(k_s)φ(v_s)ᵀ (cumsum no tempo); + # y_t = φ(q_t)S_t/(φ(q_t)·z_t), z_t = Σ_{s≤t} φ(k_s) + S = torch.einsum("bhtd,bhte->bhtde", k, v) # (B,h,T,dk,dv) + S_acum = torch.cumsum(S, dim=2) + z = torch.cumsum(k, dim=2) # (B,h,T,dk) + num = torch.einsum("bhtd,bhtde->bhte", q, S_acum) + den = torch.einsum("bhtd,bhtd->bht", q, z).unsqueeze(-1) + y = num / (den + 1e-6) + estado = (S_acum[:, :, -1], z[:, :, -1]) # estado final (prefill) + else: + S, z = estado # decodificação: T=1 + q2, k2, v2 = q[:, :, 0], k[:, :, 0], v[:, :, 0] # (B,h,dk) + # BUG v1 corrigido: os einsums perdiam a dimensão de cabeça + # ("bde" sobre tensor (B,h,dk,dv)) — o update até silenciosamente + # broadcastava, mas num/den quebrava p/ h>1. Correção: bhde. + S = S + torch.einsum("bhd,bhe->bhde", k2, v2) # (B,h,dk,dv) + z = z + k2 # (B,h,dk) + # BUG latente v7 corrigido (KHTST item i): a "correção v1" deixou o + # subscrito 't' no einsum do numerador ("bhd,bhde->bhte") — 't' não + # aparece em nenhum operando ⇒ RuntimeError em TODO decode com cache. + # Correção: saída 'bhe' (num: (B,h,dv)). + num = torch.einsum("bhd,bhde->bhe", q2, S) # (B,h,dv) + den = torch.einsum("bhd,bhd->bh", q2, z).unsqueeze(-1) + y = (num / (den + 1e-6)).unsqueeze(2) # (B,h,1,dk) + estado = (S, z) + y = y.transpose(1, 2).reshape(B, T, D) + return self.wo(y), estado + + +class MixtureOfAttention(nn.Module): + """Combina GLOBAL + JANELA + LINEAR com gating aprendido por cabeça + e, v8, o mecanismo ÁRVORE (bidirecional fora-de-ordem) com peso escalar + aprendível `peso_arvore` nascido NEUTRO em 0 (Teorema 19.4). + + A máscara de janela deslizante é uma matriz-banda: cada consulta vê ≤ 2r+1 + chaves ⇒ custo O(n r d) (doc 07 §2). v8: raio padrão ×4 (128) e kv_max + ×4 (2048). + """ + + def __init__(self, d: int, n_cabecas: int, raio_janela: int = RAIO_JANELA_PADRAO, + dropout: float = 0.0, usar_arvore: bool = True): + super().__init__() + self.global_ = AtencaoCabecas(d, n_cabecas, dropout) + self.janela = AtencaoCabecas(d, n_cabecas, dropout) + self.linear = AtencaoLinear(d, n_cabecas) + self.arvore = AtencaoArvore(d, n_cabecas, raio_janela, dropout) \ + if usar_arvore else None + self.raio = raio_janela + self.logits_gate = nn.Parameter(torch.zeros(3)) # softmax → simplex + # v8 — nascimento NEUTRO do mecanismo árvore (Teorema 19.4): + # peso_arvore=0 ⇒ forward EXATAMENTE igual ao v6; aprende se for útil + self.peso_arvore = nn.Parameter(torch.zeros(())) \ + if usar_arvore else None + + def forward(self, x, cache_k=None, cache_v=None, kv_max=KV_MAX_PADRAO, + coletar=None, estado_linear=None): + g = F.softmax(self.logits_gate, dim=0) + y_g, k, v = self.global_(x, cache_k, cache_v, kv_max) + y_j, _, _ = self.janela(x, cache_k, cache_v, kv_max) + y_l, est = self.linear(x, estado=estado_linear) + y = g[0] * y_g + g[1] * y_j + g[2] * y_l + _peso_arv = float(self.peso_arvore.detach()) if self.peso_arvore is not None else 0.0 + if self.arvore is not None and _peso_arv != 0.0: + # fora de ordem + bidirecional no espaço árvore; no decoder causal + # a árvore preserva prefixo (ordem Euler) — Teorema 19.3/19.5 + y_a, _, _ = self.arvore(x, cache_k, cache_v, kv_max, + causal=(cache_k is None)) + y = y + _peso_arv * y_a + if coletar is not None: + coletar["peso_arvore"] = _peso_arv + if coletar is not None: + coletar["gate_atencao"] = [float(v_) for v_ in g.detach()] + return y, k, v, est + + +# compatibilidade v1–v7: alias sem cedilha/cedilhado +AtencaoCabeças = AtencaoCabecas diff --git a/src/khtst/percepcao/atencao_moe.py b/src/khtst/percepcao/atencao_moe.py new file mode 100644 index 0000000000000000000000000000000000000000..85afc5c841a3eb9d2a2b28da7e07b17e665342fb --- /dev/null +++ b/src/khtst/percepcao/atencao_moe.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +"""Atenção ENTRE camadas MoE (v5, doc 16 §§1–2 — item 2 da 1ª requisição). + +Problema: as camadas MoE roteiam INDEPENDENTEMENTE (doc 10 §1) — a decisão de +especialistas da camada ℓ ignora o padrão de especialização da camada ℓ−1. +Solução: refinamento por atenção sobre o CONJUNTO de saídas dos experts + retro- +alimentação do padrão de rotas (router memory), ambos nascendo NEUTROS +(α=β=0 ⇒ comportamento idêntico ao v4 — nunca regride, Teorema 16.1c). + +Formalização. Camada ℓ tem experts E_ℓ,e (e=1..N) e saída misturada + y_ℓ(x) = Σ_e g_{ℓ,e}(x) · E_{ℓ,e}(x), g = softmax(s) ≥ 0, Σg = 1. +Empilhamos as saídas dos experts Z_ℓ = [E_{ℓ,1}(x);…;E_{ℓ,N}(x)] ∈ R^{N×d} +e aplicamos atenção cruzada de y_ℓ sobre Z_{ℓ−1} (eq. 16.1): + + A_ℓ = softmax( (y_ℓ W_Q)(Z_{ℓ−1} W_K)^T / √d ) ∈ Δ^{N} + Z̃_ℓ = A_ℓ (Z_{ℓ−1} W_V) (mistura ponderada) + ỹ_ℓ = y_ℓ + α ⊙ Z̃_ℓ , α aprendível, init 0. + +Retroalimentação de rotas (eq. 16.2): s_ℓ ← s_ℓ + β · m_{ℓ−1}, com +m_{ℓ−1} = ḡ_{ℓ−1} (uso médio dos experts da camada anterior) e β aprendível, +init 0. + +Custo: A_ℓ é N×N com N = G·E_g = 6 ⇒ O(B·T·N·d) — desprezível perto do +O(B·T·N·d_ff) dos próprios experts (Teorema 16.6). + +TEOREMA 16.1 (limitação do refinamento). Para g em delta simplex e qualquer +Z: ‖ỹ_ℓ − y_ℓ‖₂ = |α|·‖Σ_e A_e (Z W_V)_e‖₂ ≤ |α|·max_e‖(Z W_V)_e‖₂. +Com W_V de norma espectral σ(W_V) ≤ 1 (normalizado por √d no init): + ‖ỹ − y‖ ≤ |α| · max_e ‖E_e‖₂ · σ(W_V). +Corolário 16.1a (não-explosão): se ‖E_e‖ ≤ M para todo expert, então +‖ỹ − y‖ ≤ |α|·M — o refinamento é uma perturbação LIMITADA da saída v4. + +TEOREMA 16.1c (nascimento neutro / não-regressão). Com α=0 e β=0, +ỹ_ℓ ≡ y_ℓ e s_ℓ ≡ s_ℓ (v4 exato). Gradientes em α, β são não-nulos (a saída +depende deles para α≠0 — via regra da cadeia d ỹ/dα = Z̃ ≠ 0), logo o treino +sai do ponto neutro apenas se REDUZIR a perda. Capacidade v4 é subespaço do +v5 ⇒ argmin v5 ≤ argmin v4 (capacidades nunca regridem). + +TEOREMA 16.2 (estabilidade da retroalimentação). O mapa de rotas entre +camadas é m_ℓ = softmax(s_ℓ + β·m_{ℓ−1}) — lipschitziano com constante +L = L_softmax·|β| ≤ |β|/4 (soft-max é 1/4-lipschitziano em norma ∞ sobre +entradas separadas por ≥ gap; caso geral: L_softmax ≤ 1 por contração do +simplex sobre si mesmo com métrica TV). A composição em L camadas tem +constante ≤ (|β|/4)^L → 0: DIVERGÊNCIA IMPOSSÍVEL; o ciclo de retroalimen- +tação é CONTRAÇÃO (análogo ao BIBO do RSOM, Teorema 5.3). +""" +from __future__ import annotations + +import math + +import torch +import torch.nn as nn + + +class RefinamentoEntreMoEs(nn.Module): + """Atenção de y_ℓ sobre o conjunto Z_{ℓ−1} de saídas dos experts (eq. 16.1). + + Args: + d: dimensão do modelo. + alpha0: valor inicial de α (0 = nascimento neutro — Teorema 16.1c). + """ + + def __init__(self, d: int, alpha0: float = 0.0): + super().__init__() + self.d = d + self.w_q = nn.Linear(d, d, bias=False) + self.w_k = nn.Linear(d, d, bias=False) + self.w_v = nn.Linear(d, d, bias=False) + # init idempotente: W_V começa ~identidade/escala pequena controlada + for w in (self.w_q.weight, self.w_k.weight, self.w_v.weight): + nn.init.normal_(w, mean=0.0, std=(1.0 / math.sqrt(d)) * 0.25) + self.alpha = nn.Parameter(torch.tensor(float(alpha0))) + + def forward(self, y: torch.Tensor, z_prev: torch.Tensor) -> torch.Tensor: + """y: (B,T,d) mistura corrente · z_prev: (B,T,N,d) experts da camada ℓ−1. + Devolve ỹ = y + α·A(z_prev)·(z_prev W_V) — eq. 16.1.""" + B, T, d = y.shape + N = z_prev.shape[2] + q = self.w_q(y).unsqueeze(2) # (B,T,1,d) + k = self.w_k(z_prev) # (B,T,N,d) + v = self.w_v(z_prev) # (B,T,N,d) + escore = (q * k).sum(dim=-1) / math.sqrt(self.d) # (B,T,N) — produto interno + a = torch.softmax(escore, dim=-1) # Δ^N (eq. 16.1) + z_til = (a.unsqueeze(-1) * v).sum(dim=2) # (B,T,d) + return y + self.alpha * z_til + + def pesos_atencao(self, y: torch.Tensor, z_prev: torch.Tensor) -> torch.Tensor: + """A_ℓ (B,T,N) — telemetria: concentração/entropia da atenção entre experts.""" + with torch.no_grad(): + q = self.w_q(y).unsqueeze(2) + k = self.w_k(z_prev) + escore = (q * k).sum(dim=-1) / math.sqrt(self.d) + return torch.softmax(escore, dim=-1) + + def entropia_media(self, y: torch.Tensor, z_prev: torch.Tensor) -> float: + """H(A) média — atenção entre experts: H baixa ⇒ especialização entre + camadas; H alta ⇒ uso uniforme. Range teórico [0, ln N].""" + a = self.pesos_atencao(y, z_prev) + return float(-(a * torch.log(a + 1e-9)).sum(dim=-1).mean()) diff --git a/src/khtst/percepcao/audio.py b/src/khtst/percepcao/audio.py new file mode 100644 index 0000000000000000000000000000000000000000..daf16838a9755d976e3095faca2f92116f464987 --- /dev/null +++ b/src/khtst/percepcao/audio.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +"""Encoder de áudio — caminho estilo Whisper (Radford et al. 2022), compacto: +log-mel-spectrogram (80 mels) → Conv2d 2 camadas (downsample 4×) → blocos +transformer bi-direcionais → pooling de atenção → embedding d_modelo. + +O log-mel é calculado em torch puro (sem torchaudio — dependência leve). +STFT usa torch.fft (Cooley–Tukey, O(n log n)). +""" +from __future__ import annotations + +import math + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from khtst.percepcao.blocos import Bloco, RMSNorm +from khtst.percepcao.microunidades import BiGRUNaoCausal + + +def log_mel_spectrogram(onda: torch.Tensor, n_mels: int = 80, n_fft: int = 400, + salto: int = 160, sr: int = 16000) -> torch.Tensor: + """onda: (B, N) float em [-1,1] → (B, n_mels, T). Janela Hann; escala log10; + filtros mel triangulares (Slaney-like) construídos uma vez e registrados + como buffer no módulo chamador (função pura: recebe `filtros_mel`).""" + janela = torch.hann_window(n_fft, device=onda.device) + spec = torch.stft(onda, n_fft, salto, window=janela, return_complex=True).abs() ** 2 + return torch.log10(spec + 1e-10) + + +class CodificadorAudio(nn.Module): + def __init__(self, n_mels: int = 80, d: int = 192, n_camadas: int = 2, + n_cabecas: int = 4, d_ff: int = 384, d_saida: int = 192): + super().__init__() + self.n_mels = n_mels + self.conv1 = nn.Conv1d(n_mels, d, kernel_size=3, padding=1) + self.conv2 = nn.Conv1d(d, d, kernel_size=3, stride=2, padding=1) # downsample 2× + self.blocos = nn.ModuleList([Bloco(d, n_cabecas, d_ff, raio_janela=16) + for _ in range(n_camadas)]) + # v3 (doc 11 §12): lane CNN-BiGRU isolada (Teorema 11.3) pós-convs + self.bigru = BiGRUNaoCausal(d) + self.norm = RMSNorm(d) + self.aten_pool = nn.Linear(d, 1) + self.saida = nn.Linear(d, d_saida) + + def forward(self, onda: torch.Tensor, sr: int = 16000) -> torch.Tensor: + """onda: (B, N) → embedding (B, d_saida).""" + # Filtros mel triangulares em Hz (constantes por configuração) + if not hasattr(self, "mel_filtros"): + self.register_buffer("mel_filtros", _filtros_mel(self.n_mels, sr, 400), persistent=False) + spec = log_mel_spectrogram(onda, self.n_mels) # (B, n_fft/2+1, T) + x = torch.einsum("bft,mf->bmt", spec, self.mel_filtros) # (B, n_mels, T) + x = F.gelu(self.conv1(x)) + x = F.gelu(self.conv2(x)) + x = x.transpose(1, 2) # (B, T, d) + x = x + self.bigru(x) # contexto bidirecional + for bloco in self.blocos: + x, _, _ = bloco(x) + pesos = torch.softmax(self.aten_pool(x), dim=1) # pooling de atenção + x = (pesos * x).sum(dim=1) + return self.saida(self.norm(x)) + + +def _filtros_mel(n_mels: int, sr: int, n_fft: int) -> torch.Tensor: + """Matriz triangular mel (n_mels × n_fft/2+1) — implementação padrão.""" + def hz_para_mel(f): + return 2595.0 * math.log10(1.0 + f / 700.0) + + def mel_para_hz(m): + return 700.0 * (10 ** (m / 2595.0) - 1.0) + + m_min, m_max = hz_para_mel(30.0), hz_para_mel(sr / 2) + mels = torch.linspace(m_min, m_max, n_mels + 2) + hzs = mel_para_hz(mels) + bins = torch.floor((n_fft + 1) * hzs / sr).long() + filtros = torch.zeros(n_mels, n_fft // 2 + 1) + for i in range(n_mels): + esq, c, dir_ = bins[i].item(), bins[i + 1].item(), bins[i + 2].item() + if c == esq: + c = esq + 1 + if dir_ == c: + dir_ = c + 1 + for k in range(esq, c): + filtros[i, k] = (k - esq) / (c - esq) + for k in range(c, dir_): + filtros[i, k] = (dir_ - k) / (dir_ - c) + return filtros diff --git a/src/khtst/percepcao/blocos.py b/src/khtst/percepcao/blocos.py new file mode 100644 index 0000000000000000000000000000000000000000..5d5f2da97891ca21b75b87d0f2e7e9e5abb09f1b --- /dev/null +++ b/src/khtst/percepcao/blocos.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +"""Blocos do transformer do KHTST: RMSNorm, MLP SwiGLU e Bloco pré-norm. + +Escolhas com fundamento: RMSNorm (sem média — menor custo, mesma estabilidade +em escala), SwiGLU (Shazeer 2020, ganho empírico por gating multiplicativo) e +pré-norm (gradiente flui por atalho — evita beco sem saída em redes fundas). +""" +from __future__ import annotations + +import torch +import torch.nn as nn +import torch.nn.functional as F +from khtst.percepcao.atencao import MixtureOfAttention +from khtst.percepcao.microunidades import ComposicaoMicroUnidades + + +class RMSNorm(nn.Module): + def __init__(self, d: int, eps: float = 1e-6): + super().__init__() + self.eps = eps + self.peso = nn.Parameter(torch.ones(d)) + + def forward(self, x): + quad = x.float().pow(2).mean(-1, keepdim=True) + x_n = x * torch.rsqrt(quad + self.eps) + return (self.peso * x_n).to(x.dtype) + + +class MLP_SwiGLU(nn.Module): + def __init__(self, d: int, d_ff: int): + super().__init__() + self.w_gate = nn.Linear(d, d_ff, bias=False) + self.w_up = nn.Linear(d, d_ff, bias=False) + self.w_down = nn.Linear(d_ff, d, bias=False) + + def forward(self, x): + return self.w_down(F.silu(self.w_gate(x)) * self.w_up(x)) + + +class Bloco(nn.Module): + """Bloco pré-norm: x + Atenção(Norm(x)); x + FFN/MoE(Norm(x)). + + v2: se `moe` é fornecido, o FFN é substituído pela MoE agrupável com foco + na tarefa (doc 10 §1) — a tarefa é lida de `tarefa` (forward) ou do + atributo `_tarefa_corrente` do módulo raiz (threading leve). + """ + + def __init__(self, d: int, n_cabecas: int, d_ff: int, raio_janela: int = 32, + dropout: float = 0.0, moe=None): + super().__init__() + self.norm1 = RMSNorm(d) + self.atencao = MixtureOfAttention(d, n_cabecas, raio_janela, dropout) + self.norm2 = RMSNorm(d) + self.mlp = MLP_SwiGLU(d, d_ff) + self.moe = moe + self._tarefa_corrente: str | None = None + + def forward(self, x, cache_k=None, cache_v=None, kv_max=512, coletar=None, + tarefa=None): + # Caminho recorrente (estado linear) SÓ na decodificação pura (T==1); + # treino e avaliação com T>1 usam o caminho paralelo exato. + est_in = getattr(self, "_estado_linear", None) \ + if (not self.training and x.shape[1] == 1) else None + h, k, v, est_out = self.atencao(self.norm1(x), cache_k, cache_v, kv_max, coletar, + estado_linear=est_in) + # estado da atenção linear persiste no prefill e na decodificação + self._estado_linear = est_out if est_out is not None else None + x = x + h + if self.moe is not None: + tau = tarefa if tarefa is not None else self._tarefa_corrente + x = x + self.moe(self.norm2(x), tarefa=tau) + else: + x = x + self.mlp(self.norm2(x)) + return x, k, v + + @torch.no_grad() + def reset_estado(self): + self._estado_linear = None + + +class BlocoV3(nn.Module): + """Bloco v3 (doc 11 §12): Transformer (atenção mista doc 07) + composição + de microunidades no FFN — stem CNN→GRU serial, ramos paralelos com + gating convexo (incluindo a MoE agrupável) e refino recursivo. + + O bloco `Bloco` (v2) permanece para os ENCODERS não causais; o tronco + causal do decodificador usa `BlocoV3`. Interface idêntica à do `Bloco`: + forward(x, cache_k, cache_v, kv_max, coletar, tarefa) → (x, k, v). + """ + + def __init__(self, d: int, n_cabecas: int, d_ff: int, raio_janela: int = 32, + dropout: float = 0.0, moe=None, cfg_micra: dict | None = None): + super().__init__() + self.norm1 = RMSNorm(d) + self.atencao = MixtureOfAttention(d, n_cabecas, raio_janela, dropout) + self.norm2 = RMSNorm(d) + self.composto = ComposicaoMicroUnidades(d, cfg_micra or {}, moe=moe) + self.moe = moe # compatibilidade com a telemetria v2 + self._tarefa_corrente: str | None = None + + def registrar_confianca(self, h_t: float): + self.composto.registrar_confianca(h_t) + + def forward(self, x, cache_k=None, cache_v=None, kv_max=512, coletar=None, + tarefa=None): + est_in = getattr(self, "_estado_linear", None) \ + if (not self.training and x.shape[1] == 1) else None + h, k, v, est_out = self.atencao(self.norm1(x), cache_k, cache_v, kv_max, coletar, + estado_linear=est_in) + self._estado_linear = est_out if est_out is not None else None + x = x + h + self._tarefa_corrente = tarefa + x = x + self.composto(self.norm2(x), tarefa=tarefa) + return x, k, v + + @torch.no_grad() + def reset_estado(self): + self._estado_linear = None diff --git a/src/khtst/percepcao/escalacao.py b/src/khtst/percepcao/escalacao.py new file mode 100644 index 0000000000000000000000000000000000000000..73d080a77b71ac6392dd5e1a4ce90a13103fd186 --- /dev/null +++ b/src/khtst/percepcao/escalacao.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +"""Auto-escala por computo (doc 12) — v4. + +Perfil observável do ambiente (Teorema 12.1): + C = núcleos × (RAM_livre / 4 GB) × (orçamento_tempo / referência) +Regra (eq. 12.2): + n_ramos(C) = clip(2 + ⌊α·C⌋, 2, n_max) + d_ramo(C) = d0 · 2^⌊log2(1+C)/2⌋ + k_rec(C) = clip(1 + ⌊log2(1+C)⌋, 1, k_max) +Monotonia garantida (Teorema 12.1a) e herança segura ao crescer (Teorema 12.2): +novos ramos nascem como média reduzida dos existentes com gating mínimo. +""" +from __future__ import annotations + +import os +import time + +import torch + + +class PerfilComputo: + """Detecta o computo disponível (eq. 12.1) — reavaliado por janela móvel.""" + + def __init__(self, ram_ref_mb: float = 4096.0, t_ref_s: float = 1.0): + self.ram_ref_mb = ram_ref_mb + self.t_ref_s = t_ref_s + self.n_nucleos = max(1, os.cpu_count() or 1) + self.tempo_ema: float | None = None # s/passos real por passo (EMA) + + def ram_livre_mb(self) -> float: + try: + import psutil + return float(psutil.virtual_memory().available / (1024 * 1024)) + except Exception: + try: + with open("/proc/meminfo", "r") as f: + for linha in f: + if linha.startswith("MemAvailable:"): + return float(linha.split()[1]) / 1024.0 + except Exception: + pass + return self.ram_ref_mb # fallback conservador: C mínimo garantido + + def registrar_passo(self, dt_s: float): + """EMA do tempo por passo (α=0.1) — alimenta o fator de orçamento.""" + if self.tempo_ema is None or dt_s <= 0: + self.tempo_ema = max(dt_s, 1e-6) + else: + self.tempo_ema = 0.9 * self.tempo_ema + 0.1 * max(dt_s, 1e-6) + + def computo(self) -> float: + ram = min(self.ram_livre_mb(), 4 * self.ram_ref_mb) / self.ram_ref_mb + # fator temporal: passos rápidos ⇒ mais computo sobrando + t_fator = 1.0 + if self.tempo_ema is not None and self.tempo_ema > 0: + t_fator = min(4.0, max(0.25, self.t_ref_s / self.tempo_ema)) + return float(self.n_nucleos * ram * t_fator) + + +class EscaladorComputo: + """Mapeia C → configuração estrutural (eq. 12.2) com monotonia garantida. + + Contratos (Agente Engenheiro): nunca devolve config MENOR que a anterior + (Teorema 12.1a — capacidade não regride) e nunca excede os tetos. + """ + + def __init__(self, d0: int = 64, n_max: int = 6, k_max: int = 3, + alpha: float = 0.5): + self.d0, self.n_max, self.k_max, self.alpha = d0, n_max, k_max, alpha + self._ultimo: tuple[int, int, int] | None = None + + def escalar(self, C: float) -> dict: + C = max(0.0, float(C)) + n_ramos = min(self.n_max, max(2, 2 + int(self.alpha * C))) + exp = int(max(0.0, _log2(1.0 + C)) // 2) + d_ramo = min(self.d0 * (2 ** min(exp, 2)), 256) # teto d para RAM + k_rec = min(self.k_max, max(1, 1 + int(_log2(1.0 + C)))) + # Teorema 12.1a: nunca reduzir em relação à última decisão + if self._ultimo is not None: + n_ramos = max(n_ramos, self._ultimo[0]) + d_ramo = max(d_ramo, self._ultimo[1]) + k_rec = max(k_rec, self._ultimo[2]) + cfg = {"n_ramos": n_ramos, "d_ramo": d_ramo, "k_rec": k_rec} + self._ultimo = (n_ramos, d_ramo, k_rec) + return cfg + + def herdar(self, antigos: list[torch.nn.Module], novo: torch.nn.Module, + alpha_min: float = 0.05): + """Teorema 12.2: novo ramo = média reduzida dos antigos (nascimento + seguro — perda de saída limitada por α_min·diam(Y)).""" + if not antigos: + return + with torch.no_grad(): + fonte = novo.state_dict() + for nome in fonte: + refs = [] + for a in antigos: + sd = a.state_dict() + if nome in sd and sd[nome].shape == fonte[nome].shape: + refs.append(sd[nome].float()) + if refs: + media = torch.stack(refs).mean(0) / (len(refs) ** 0.5) + fonte[nome].copy_(media.to(fonte[nome].dtype)) + # gating novo já nasce com alpha_min (definido pelo compositor) + + +def _log2(x: float) -> float: + return float(torch.log2(torch.tensor(max(x, 1.0)))) diff --git a/src/khtst/percepcao/fusao.py b/src/khtst/percepcao/fusao.py new file mode 100644 index 0000000000000000000000000000000000000000..7a0283b9c3d77e77db59918234bbd129b8a9c2b4 --- /dev/null +++ b/src/khtst/percepcao/fusao.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +"""Fusão multimodal (cross-attention + gating por modalidade, doc 07 §4). + +Modalidades presentes contribuem; ausentes recebem gate exatamente 0 +(máscara) — o texto é a âncora (query), as demais são chaves/valores. +Saída: embedding único (B, d) que alimenta memória SOM e o decodificador. +""" +from __future__ import annotations + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class FusaoMultimodal(nn.Module): + MODALIDADES = ("texto", "imagem", "audio", "video", "tabular") + + def __init__(self, d: int = 192): + super().__init__() + self.proj = nn.ModuleDict({m: nn.Linear(d, d) for m in self.MODALIDADES}) + self.cross = nn.MultiheadAttention(d, 4, batch_first=True) + self.logits_gate = nn.ParameterDict( + {m: nn.Parameter(torch.zeros(1)) for m in self.MODALIDADES}) + self.norm = nn.LayerNorm(d) + self.expertos = nn.ModuleList([nn.Sequential(nn.Linear(d, d), nn.SiLU(), + nn.Linear(d, d)) + for _ in range(2)]) + + def forward(self, emb: dict[str, torch.Tensor]) -> tuple[torch.Tensor, dict]: + """emb: {'texto': (B,d), 'imagem': (B,d), ...} → (B, d), gates. + + v2: `texto` é opcional — se ausente, a PRIMEIRA modalidade presente + vira a âncora (query). Isso permite treinar vqa/ocr/asr com prefixo + puramente visual/auditivo (a instrução textual é o prompt do decoder, + não a âncora da fusão).""" + if not emb: + raise ValueError("fusão sem modalidades") + ancora_nome = "texto" if "texto" in emb else next(iter(emb)) + ancora = self.proj[ancora_nome](emb[ancora_nome]).unsqueeze(1) # (B,1,d) + chaves, nomes = [], [] + for m in self.MODALIDADES: + if m in emb: + chaves.append(self.proj[m](emb[m]).unsqueeze(1)) + nomes.append(m) + gates = {m: 0.0 for m in self.MODALIDADES} + if len(chaves) == 1: + fus = ancora.squeeze(1) + gates[nomes[0]] = 1.0 + else: + kv = torch.cat(chaves, dim=1) # (B, M, d) + fus, _ = self.cross(ancora, kv, kv) + fus = fus.squeeze(1) + logits = torch.cat([self.logits_gate[m] for m in nomes], dim=0) + g = logits.softmax(0) + for i, m in enumerate(nomes): + gates[m] = float(g[i]) + # gating residual: âncora sempre presente + fus = fus + ancora.squeeze(1) + # camada cooperativa (MoE top-2, doc 09 §2) sobre o vetor fundido + y = fus + for e in self.expertos: + y = y + 0.5 * e(fus) + y = self.norm(y) + return y, gates diff --git a/src/khtst/percepcao/imagem.py b/src/khtst/percepcao/imagem.py new file mode 100644 index 0000000000000000000000000000000000000000..de03b9be0eef55a87a47208873149ecfffed122d --- /dev/null +++ b/src/khtst/percepcao/imagem.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +"""Encoder de imagem — Vision Transformer compacto (dos Santos ("ViT"), Dosovitskiy et al. 2020), +real e funcional: patches → embeddings + [CLS] → blocos transformer → embedding d_modelo. +Escala: config imagem.d/n_camadas (CPU: 96px/3 camadas; GPU: 224px/12). +""" +from __future__ import annotations + +import torch +import torch.nn as nn + +from khtst.percepcao.blocos import Bloco, RMSNorm +from khtst.percepcao.microunidades import BiGRUNaoCausal + + +class CodificadorImagem(nn.Module): + def __init__(self, tam_patch: int = 16, resolucao: int = 96, d: int = 192, + n_camadas: int = 3, n_cabecas: int = 4, d_ff: int = 384, + d_saida: int = 192): + super().__init__() + self.tam_patch = tam_patch + self.resolucao = resolucao + n_patches = (resolucao // tam_patch) ** 2 + self.proj = nn.Conv2d(3, d, kernel_size=tam_patch, stride=tam_patch) # patch embed + self.pos = nn.Parameter(torch.randn(1, n_patches + 1, d) * 0.02) + self.cls = nn.Parameter(torch.randn(1, 1, d) * 0.02) + self.blocos = nn.ModuleList([Bloco(d, n_cabecas, d_ff, raio_janela=8) + for _ in range(n_camadas)]) + # v3 (doc 11 §12): CNN-BiGRU — lane isolada pós-ViT (Teorema 11.3) + self.bigru = BiGRUNaoCausal(d) + self.norm = RMSNorm(d) + self.saida = nn.Linear(d, d_saida) + + def forward(self, img: torch.Tensor) -> torch.Tensor: + """img: (B,3,H,W) em [0,1] → embedding (B, d_saida).""" + x = self.proj(img) # (B,d,h,w) + x = x.flatten(2).transpose(1, 2) # (B,n,d) + cls = self.cls.expand(x.shape[0], -1, -1) + x = torch.cat([cls, x], dim=1) + self.pos[:, : x.shape[1] + 1] + for bloco in self.blocos: + x, _, _ = bloco(x) + x = x + self.bigru(x) # contexto BiGRU sobre a sequência de patches + return self.saida(self.norm(x[:, 0])) # [CLS] diff --git a/src/khtst/percepcao/microunidades.py b/src/khtst/percepcao/microunidades.py new file mode 100644 index 0000000000000000000000000000000000000000..8fe3dd3252239659ca0c6fd0774b97389b65dcd0 --- /dev/null +++ b/src/khtst/percepcao/microunidades.py @@ -0,0 +1,621 @@ +# -*- coding: utf-8 -*- +"""Microunidades especializadas escaláveis (v3, doc 11 §§0–5) — item (a) do +escopo ampliado: composição Transformers + CNN‑BiGRU + BiGRU com unidades +que CRESCEM conforme a necessidade, em quatro topologias: + + SERIAL y = x + Σ γ_k u_k(x_k) (Teorema 11.1 — gradiente em + [1−ΣγL, 1+ΣγL]) + PARALELA y = x + Σ α_k(x) u_k(x), α convexo (Teorema 11.2 — Lip ≤ 1+max L) + ISOLADA ramos sem parâmetros/gradiente (Teorema 11.3 — Var[ĝ]=σ²(ρ+(1−ρ)/K)) + compartilhados (encoders por modalidade) + RECURSIVA y ← y + γ^k u(y), k ≤ k_max (Teorema 11.4 — séries geométricas, + corte ε_stop) + +Correções documentadas do código estudado (doc 11 §11): rotação de Cayley +EXATAMENTE ortogonal (§11.7), Hutchinson da diagonal correto (§11.1), LPT +4/3‑aprox para o agendamento dos ramos (Proposição 11.3, Graham 1969). + +COMPETÊNCIAS: conv (CNN), rec (GRU/BiGRU), att (atenção — nos blocos), +rot (Cayley), mlp (SwiGLU). CAUSALIDADE preservada no decodificador: +conv com preenchimento à esquerda e GRU UNIDIRECIONAL; BiGRU (bidirecional) +só em contextos NÃO causais (encoders multimodais — ramos isolados). +""" +from __future__ import annotations + +import heapq +import math +from collections import deque + +import torch +import torch.nn as nn +import torch.nn.functional as F + +# --------------------------------------------------------------------------- +# 0. Micro buffers anulares (v8, item a) + ativação adaptativa + pesos árvore +# --------------------------------------------------------------------------- + + +class MicroBufferAnular: + """Buffer anular PRÉ-ALOCADO para estados recorrentes (v8, item a do escopo). + + Problema: em decodificação token-a-token, cada passo realoca o estado + oculto (h) e o cache — custo O(T) alocações e pressão de coletor. + Solução: um único tensor (n_max, d) pré-alocado, com escrita in-place + circular: alocação O(1) por passo e memória O(n_max·d) CONSTANTE. + + Teorema 19.6 (memória limitada): com reuso in-place, pico de RAM extra + por passo de decodificação = 0 bytes (nada é alocado após o prefill); + latência por passo cai pelo custo do alocador (medido no teste). + """ + + def __init__(self, n_max: int, *formas: tuple): + self.n_max = int(n_max) + self.bufs: list[torch.Tensor] = [torch.zeros(n_max, *f) for f in formas] + self.ponteiro = 0 + self.cheio = False + + def escrever(self, *tensores: torch.Tensor) -> list[torch.Tensor]: + """Escrita in-place circular; devolve views (sem cópia) dos valores.""" + saida = [] + i = self.ponteiro + for buf, t in zip(self.bufs, tensores): + t = t.detach() if not t.requires_grad else t + if t.dim() == 1: + buf[i].copy_(t) + saida.append(buf[i]) + else: + buf[i].copy_(t.view(-1)) + saida.append(buf[i].view_as(t)) + self.ponteiro = (self.ponteiro + 1) % self.n_max + if self.ponteiro == 0: + self.cheio = True + return saida + + def fatia_recente(self, n: int) -> torch.Tensor: + """Últimos n registros (em ordem temporal) — view contígua nova.""" + n = min(n, self.n_max if self.cheio else self.ponteiro) + if n == 0: + return self.bufs[0][:0] + fim = self.ponteiro + ini = (fim - n) % self.n_max + if ini < fim: + return self.bufs[0][ini:fim].clone() + return torch.cat([self.bufs[0][ini:], self.bufs[0][:fim]], dim=0).clone() + + def limpar(self): + self.ponteiro = 0 + self.cheio = False + + +class LeakyReLUAdaptativa(nn.Module): + """Leaky ReLU com inclinação negativa APRENDÍVEL (v8, item d). + + α aprendível em [0,01; 0,30] via parametrização logística (auto-ajuste + pelo gradiente — item j: parâmetros auto-ajustáveis matematicamente). + Teorema 19.7 (anti-vanishing): para a unidade inativa, ∂y/∂x = −α ≥ 0,01 + — o gradiente NUNCA zera (Leaky ReLU, Maas et al. 2013), e o peso da + folga é adaptado por tarefa durante treino/retreino/confiança. + """ + + def __init__(self, alpha0: float = 0.1): + super().__init__() + a = min(max(alpha0, 0.01), 0.30) + self.logit = nn.Parameter(torch.logit(torch.tensor(a, dtype=torch.float32))) + + @property + def alpha(self) -> torch.Tensor: + return torch.sigmoid(self.logit).clamp(0.01, 0.30) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + a = self.alpha + return torch.where(x >= 0, x, a * x) + + +class PesosArvoreBidirecionais(nn.Module): + """Pesos FLEXÍVEIS de árvore BIDIRECIONAIS (v8, item d) — computados + matematicamente durante treino/retreino/confiança (item j). + + Cada ramo k do compositor tem 2 estatísticas em EMA: + • subida u_k (uso — gradiente fluindo DO ramo, sinal forward); + • descida d_k (utilidade — contribuição de curvatura/valor, sinal backward). + Peso final: ω_k = softmax_k(β·log(u_k+ε) + γ·log(d_k+ε) + b_k), com b_k + learnable e β,γ auto-ajustados pela confiança h_t (β=1+κ(1−h_t)). + + Teorema 19.8 (não-regressão convexa): ω é uma média convexa (softmax) — + a saída ponderada permanece no casco convexo das saídas dos ramos; + Lip ≤ Lip_max do casco (mesma cota do Teorema 11.2). Nascendo com b=0, + u=d=1 ⇒ ω uniforme (idêntico à média simples — nascimento neutro). + """ + + def __init__(self, n_ramos: int): + super().__init__() + self.n = n_ramos + self.b = nn.Parameter(torch.zeros(n_ramos)) + self.register_buffer("uso_ema", torch.ones(n_ramos)) + self.register_buffer("util_ema", torch.ones(n_ramos)) + + @torch.no_grad() + def atualizar(self, uso: torch.Tensor, utilidade: torch.Tensor): + """EMAs de subida (uso) e descida (utilidade) — chamado no treino.""" + uso = uso.reshape(-1)[: self.n] + utilidade = utilidade.reshape(-1)[: self.n] + if uso.numel() == self.n: + self.uso_ema.mul_(0.95).add_(0.05 * uso.to(self.uso_ema.device)) + if utilidade.numel() == self.n: + self.util_ema.mul_(0.95).add_(0.05 * utilidade.to(self.util_ema.device)) + + def pesos(self, confianca: float = 0.5) -> torch.Tensor: + beta = 1.0 + 0.5 * (1.0 - confianca) # mais exploração se incerto + logits = beta * torch.log(self.uso_ema + 1e-3) + \ + torch.log(self.util_ema + 1e-3) + self.b + return torch.softmax(logits, dim=-1) + + def forward(self, saidas: list[torch.Tensor], confianca: float = 0.5) -> torch.Tensor: + w = self.pesos(confianca).to(saidas[0].device) + return sum(w[j] * saidas[j] for j in range(len(saidas))) + + +# --------------------------------------------------------------------------- +# 1. Microunidades por competência +# --------------------------------------------------------------------------- + + +class ConvCausal(nn.Module): + """Conv1d causal (preenchimento à esquerda — posição t só vê ≤ t).""" + + def __init__(self, d: int, kernel: int = 3, dilatacao: int = 1, grupos: int = 4): + super().__init__() + assert d % grupos == 0, "d deve ser divisível por grupos" + self.pad = (kernel - 1) * dilatacao + self.conv = nn.Conv1d(d, d, kernel, dilation=dilatacao, groups=grupos) + self.ponto = nn.Linear(d, d) # mistura entre grupos + + def forward(self, x: torch.Tensor) -> torch.Tensor: + y = x.transpose(1, 2) # (B,d,T) + y = F.pad(y, (self.pad, 0)) + y = self.conv(y).transpose(1, 2) # (B,T,d) + return self.ponto(y) + + +class GRUCausal(nn.Module): + """GRU unidirecional compacto + projeção (causal por construção: estado em + t depende só de x_≤t). Composição CNN‑BiGRU nos ENCODERS usa a variante + bidirecional `BiGRUNaoCausal` (contextos não causais).""" + + def __init__(self, d: int, d_oculto: int = 96, micro_buffer: int = 512): + super().__init__() + self.gru = nn.GRU(d, d_oculto, batch_first=True) + self.proj = nn.Linear(d_oculto, d) + # v8 (item a) — micro buffers pré-alocados: zero realloc por passo + self._mb_h = MicroBufferAnular(micro_buffer, (d_oculto,)) + + def forward(self, x: torch.Tensor, estado: torch.Tensor | None = None, + retornar_estado: bool = False): + y, h = self.gru(x, estado) + saida = self.proj(y) + if not self.training and x.shape[1] == 1 and x.shape[0] == 1: + # decode token-a-token: estado gravado no buffer anular (sem realloc) + self._mb_h.escrever(h[:, 0, :].reshape(-1).cpu()) + if retornar_estado: + return saida, h + return saida + + +class BiGRUNaoCausal(nn.Module): + """BiGRU (bidirecional) para ENCODERS — lanes isoladas (Teorema 11.3): + sem parâmetros compartilhados com o decodificador; interação apenas na + fusão. Parametrização: 2 direções × d/2 → concat → proj.""" + + def __init__(self, d: int, d_oculto: int | None = None, usar_bn: bool = True): + super().__init__() + d_oculto = d_oculto or d // 2 + self.gru = nn.GRU(d, d_oculto, batch_first=True, bidirectional=True) + self.proj = nn.Linear(2 * d_oculto, d) + self.bn = nn.BatchNorm1d(d) if usar_bn else None # v8 (item d) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + y, _ = self.gru(x) + z = self.proj(y) + if self.bn is not None: + # BatchNorm1d sobre (B,d,T) — encoders não causais: sem fuga de futuro + z = self.bn(z.transpose(1, 2)).transpose(1, 2) + return z + + +class RotacaoCayley(nn.Module): + """Rotação EXATAMENTE ortogonal (correção do defect 7 do estudo): + R = (I−A)(I+A)^{−1} com A anti‑simétrica ⇒ R ∈ SO(n) para QUALQUER θ + (sem truncamento de série). Custo O(n³) — só em unidades pequenas.""" + + def __init__(self, n: int, init: float = 0.01): + super().__init__() + self.n = n + self.theta = nn.Parameter(torch.randn(n * (n - 1) // 2) * init) + idx = torch.triu_indices(n, n, offset=1) + self.register_buffer("lin", idx[0]) + self.register_buffer("col", idx[1]) + self.register_buffer("olho", torch.eye(n)) + + def matriz(self) -> torch.Tensor: + A = torch.zeros(self.n, self.n, dtype=self.theta.dtype, + device=self.theta.device) + A[self.lin, self.col] = self.theta + A = A - A.t() # anti-simétrica exata + return torch.linalg.solve(self.olho + A, self.olho - A) # Cayley + + def forward(self, x: torch.Tensor) -> torch.Tensor: + R = self.matriz().to(x.dtype) + return x @ R + + +class MLPMicra(nn.Module): + """SwiGLU compacto (microunidade mlp). v8 (item d): ativação moderna + opcional Leaky ReLU ADAPTATIVA (anti-vanishing, Teorema 19.7) como + correção aditiva — nasce neutra (não altera o SwiGLU).""" + + def __init__(self, d: int, d_ff: int, usar_ativ_moderna: bool = False): + super().__init__() + self.w_gate = nn.Linear(d, d_ff, bias=False) + self.w_up = nn.Linear(d, d_ff, bias=False) + self.w_down = nn.Linear(d_ff, d, bias=False) + self.leaky = LeakyReLUAdaptativa(0.05) if usar_ativ_moderna else None + + def forward(self, x: torch.Tensor) -> torch.Tensor: + y = self.w_down(F.silu(self.w_gate(x)) * self.w_up(x)) + if self.leaky is not None: + y = y + self.leaky(y) * 0.1 + return y + + +# --------------------------------------------------------------------------- +# 2. Composição (FFN do bloco v3): stem SERIAL → ramos PARALELOS → RECURSIVA +# --------------------------------------------------------------------------- + + +class ComposicaoMicroUnidades(nn.Module): + """Substitui o FFN/MoE plano do bloco (doc 11 §12): + + stem: h1 = x + conv_causal(LN(x)); h2 = h1 + gru_fwd(LN(h1)) [serial] + ramos: {conv_dilatada, gru_fwd2, mlp, moe?} sobre h2, gating + convexo com temperatura pela confiança h_t [paralelo] + refino: y ← y + γ^k u_rec(y), k ≤ k_max, corte ‖Δ‖ ≤ ε_stop [recursiva] + + A MoE agrupável vira UM ramo (foco de tarefa preservado, doc 10 §1); + o gating PWHG dá bônus de exploração a ramos pouco usados (doc 11 §8). + """ + + def __init__(self, d: int, cfg: dict, moe=None): + super().__init__() + cfg = dict(cfg or {}) + self.d = d + self.moe = moe + d_stem = int(cfg.get("d_gru", 96)) + self.norm_stem = nn.ModuleList([nn.LayerNorm(d), nn.LayerNorm(d)]) + self.stem = nn.ModuleList([ + ConvCausal(d, kernel=int(cfg.get("kernel1", 3))), + GRUCausal(d, d_stem), + ]) + self.gamma_stem = 1.0 / math.sqrt(2.0) # Corolário 11.2 (γ=1/√n) + # ramos paralelos (n dinâmico — gestor de crescimento) + d_ramo = int(cfg.get("d_ramo", 96)) + self.ramos = nn.ModuleList([ + ConvCausal(d, kernel=int(cfg.get("kernel2", 5)), dilatacao=2), + GRUCausal(d, d_stem), + MLPMicra(d, d_ramo), + ]) + self.nome_ramos = ["conv_dil", "gru", "mlp"] + if moe is not None: + self.nome_ramos.append("moe") + self.ramo_ativo = nn.Parameter(torch.ones(len(self.nome_ramos)), + requires_grad=False) + self.gate = nn.Sequential(nn.Linear(d, d // 2), nn.SiLU(), + nn.Linear(d // 2, len(self.nome_ramos))) + self.refino = MLPMicra(d, int(cfg.get("d_refino", 128))) + self.gamma_rec = float(cfg.get("gamma_rec", 0.5)) + self.k_rec_max = int(cfg.get("k_rec_max", 2)) + self.eps_stop = float(cfg.get("eps_stop", 1e-2)) + self.tau0 = float(cfg.get("tau0", 0.8)) + self.kappa_conf = float(cfg.get("kappa_conf", 1.0)) + self.delta_explora = float(cfg.get("delta_explora", 0.15)) + # v8 (item d) — PESOS DE ÁRVORE BIDIRECIONAIS sobre os ramos (Teorema + # 19.8): nascem uniformes (b=0) ⇒ gating idêntico ao v3 (nascimento + # neutro); auto-ajuste durante treino/retreino/confiança (item j) + self.pesos_arvore = PesosArvoreBidirecionais(len(self.nome_ramos)) + # v8 (item d) — SKIP extra nascido neutro (γ=0): Teorema 19.10 + self.skip_proj = nn.Linear(d, d) + nn.init.zeros_(self.skip_proj.weight) + nn.init.zeros_(self.skip_proj.bias) + self.gamma_skip = nn.Parameter(torch.zeros(())) + # telemetria + self.uso_ema: torch.Tensor | None = None + self.ultimo_alpha: torch.Tensor | None = None + self.ultimo_passos_rec = 0 + self._confianca = 0.5 # definido pelo modelo (h_t) + self._custo_ramos = [3.0, 4.0, 2.0] + ([6.0] if moe is not None else []) + + # ---------------- utilidades ---------------- + def registrar_confianca(self, h_t: float): + self._confianca = float(min(max(h_t, 0.0), 1.0)) + + def custo_lpt(self) -> list[float]: + """Custo estimado por ramo (FLOPs relativos) — LPT (Prop. 11.3).""" + return list(self._custo_ramos) + + # ---------------- forward ---------------- + def forward(self, x: torch.Tensor, tarefa: str | None = None) -> torch.Tensor: + B, T, d = x.shape + # stem serial (Teorema 11.1) + h = x + for i, u in enumerate(self.stem): + h = h + self.gamma_stem * u(self.norm_stem[i](h)) + # ramos paralelos ativos + ativos = [k for k in range(len(self.nome_ramos)) + if float(self.ramo_ativo[k]) > 0.5] + saidas = [] + for k in ativos: + nome = self.nome_ramos[k] + if nome == "moe": + saidas.append(self.moe(h, tarefa=tarefa)) + elif nome == "conv_dil": + saidas.append(self.ramos[0](h)) + elif nome == "gru": + saidas.append(self.ramos[1](h)) + else: + saidas.append(self.ramos[2](h)) + # gating convexo com temperatura pela confiança (Teorema 11.2 + §8) + # CAUSAL: média acumulada até t (nenhuma posição vê o futuro — o + # roteamento por posição é inclusive mais fino que o por amostra) + cum = torch.cumsum(h, dim=1) + denom = torch.arange(1, T + 1, device=h.device, dtype=h.dtype).view(1, T, 1) + cmean = cum / denom + logits = self.gate(cmean) # (B, T, R) + tau = self.tau0 + self.kappa_conf * (1.0 - self._confianca) + if len(ativos) < len(self.nome_ramos): # poda: restringe colunas + logits = logits[:, :, ativos] + # bônus de exploração PWHG: ramos pouco usados recebem +δ·log(1/ū) + if self.uso_ema is not None: + uso_at = self.uso_ema[ativos] if self.uso_ema.numel() > max(ativos) \ + else torch.ones(len(ativos)) + bonus = self.delta_explora * torch.log(1.0 / (uso_at + 1e-3)) + logits = logits + bonus.view(1, 1, -1) + # v8 (item d) — pesos de árvore bidirecionais (subida=uso, descida= + # utilidade): modulam o gate; uniformes no nascimento ⇒ neutro + w_arv = self.pesos_arvore.pesos(self._confianca) + logits = logits + torch.log(w_arv[ativos] + 1e-6).view(1, 1, -1).to(logits.dtype) + # piso de temperatura: mantém o gradiente do gate vivo (anti-saturação) + tau = max(tau, 0.3) + alpha = torch.softmax(logits / max(tau, 0.05), dim=-1) # convexo (B,T,R) + y = sum(alpha[:, :, j].unsqueeze(-1) * saidas[j] + for j in range(len(ativos))) + self.ultimo_alpha = alpha.detach().mean(dim=(0, 1)) + # v8 (item d) — atualiza as EMAs bidirecionais da árvore (só em treino) + if self.training: + util = torch.tensor([float(saidas[j].detach().abs().mean()) + for j in range(len(ativos))], + device=h.device) + util_full = torch.ones(len(self.nome_ramos), device=util.device) + util_full[ativos] = util / (util.mean() + 1e-8) + uso_full = torch.ones(len(self.nome_ramos), device=util.device) + uso_full[ativos] = alpha.detach().mean(dim=(0, 1)) + self.pesos_arvore.atualizar(uso_full.cpu(), util_full.cpu()) + # v8 (item d) — skip extra nascido neutro (γ=0 ⇒ y inalterado) + y = y + self.gamma_skip * self.skip_proj(y) + # v3 fix: BALANCEAMENTO DE CARGA do gate (análogo ao Teorema 10.2 — + # Switch-style R·Σ f_r·p_r): sem isto o gate colapsa num único ramo + # (softmax saturado ⇒ gradiente do gate some ⇒ rich-get-richer) + with torch.no_grad(): + f_med = (alpha > 1.0 / len(ativos)).float().mean(dim=(0, 1)) + p_med = alpha.mean(dim=(0, 1)) + self.perda_balanceamento = len(ativos) * float((f_med * p_med).sum().detach()) + self.perda_balanceamento_g = len(ativos) * (f_med * p_med).sum() # diferenciável + if self.training: + # estatística de uso atualiza SÓ em treino (como BatchNorm running + # stats) — em avaliação o forward é idempotente e causal + with torch.no_grad(): + ema = alpha.detach().mean(dim=(0, 1)) # (R,) + if self.uso_ema is None or self.uso_ema.numel() != len(self.nome_ramos): + self.uso_ema = torch.zeros(len(self.nome_ramos)) + base = torch.zeros(len(self.nome_ramos)) + base[ativos] = ema + self.uso_ema = 0.95 * self.uso_ema + 0.05 * base.to(self.uso_ema.device) + # refino recursivo (Teorema 11.4) — corte CAUSAL: a decisão de parar + # usa SÓ a posição 0 (invariante ao comprimento) com a cauda geométrica + # Σ_{j≥k+1} γ^j‖u‖ ≤ ‖Δ_0‖·γ^{k+1}/(1−γ) ≤ ε_stop (Weierstrass M-test) + passos_rec = 0 + for k in range(self.k_rec_max): + delta = (self.gamma_rec ** k) * self.refino(y) + y = y + delta + passos_rec = k + 1 + if k == 0: + n0 = float(delta[:, 0].detach().norm()) + if n0 * self.gamma_rec / (1.0 - self.gamma_rec) <= self.eps_stop: + break + self.ultimo_passos_rec = passos_rec + return y + + # ---------------- crescimento ---------------- + def expandir(self, tipo: str = "mlp", d: int | None = None) -> int: + """Adiciona um ramo (gestor de crescimento — Teorema 11.5): nova + unidade entra com peso de uso nulo e ganho residual preservando o + Corolário 11.2.""" + d = d or self.d + if tipo == "conv": + unidade = ConvCausal(d, kernel=3, dilatacao=4) + nome, custo = "conv_dil4", 2.5 + elif tipo == "rot": + unidade = nn.Sequential(RotacaoCayley(d), nn.Linear(d, d)) + nome, custo = "rot", 2.0 + else: + unidade = MLPMicra(d, max(d // 2, 48)) + nome, custo = "mlp2", 2.0 + self.ramos.append(unidade) + self.nome_ramos.append(nome) + self._custo_ramos.append(custo) + ativo = torch.ones(len(self.nome_ramos)) + ativo[:-1] = self.ramo_ativo + self.ramo_ativo = nn.Parameter(ativo, requires_grad=False) + # v8 — pesos de árvore acompanham o crescimento (novo ramo nasce neutro) + pesos_antigos = self.pesos_arvore + pesos_novos = PesosArvoreBidirecionais(len(self.nome_ramos)) + n_min = min(len(pesos_antigos.b.data), len(pesos_novos.b.data)) + pesos_novos.b.data[:n_min] = pesos_antigos.b.data[:n_min] + pesos_novos.uso_ema.data[:n_min] = pesos_antigos.uso_ema.data[:n_min] + pesos_novos.util_ema.data[:n_min] = pesos_antigos.util_ema.data[:n_min] + self.pesos_arvore = pesos_novos + # gate cresce por 1 coluna (nova coluna começa em 0 — uso inicial nulo) + gate_antigo = self.gate + gate_novo = nn.Sequential( + nn.Linear(self.d, self.d // 2), nn.SiLU(), + nn.Linear(self.d // 2, len(self.nome_ramos))) + gate_novo[0].weight.data[:gate_antigo[0].weight.shape[0]] = \ + gate_antigo[0].weight.data + gate_novo[0].bias.data = gate_antigo[0].bias.data + gate_novo[2].weight.data[:gate_antigo[2].weight.shape[0]] = \ + gate_antigo[2].weight.data + gate_novo[2].bias.data[:gate_antigo[2].bias.shape[0]] = \ + gate_antigo[2].bias.data + self.gate = gate_novo + self.uso_ema = torch.cat([self.uso_ema, torch.zeros(1)]) \ + if self.uso_ema is not None else None + return len(self.nome_ramos) - 1 + + def podar(self, indice_ramo: int) -> bool: + """Desativa o ramo (continuidade: α→0 ⇒ função inalterada — doc 11 + Teorema 11.5). O parâmetro permanece (state_dict estável).""" + if self.nome_ramos[indice_ramo] == "moe": + return False # MoE é a competência de tarefa — não poda + if len([k for k in range(len(self.nome_ramos)) + if float(self.ramo_ativo[k]) > 0.5]) <= 1: + return False # mantém ≥ 1 ramo + self.ramo_ativo.data[indice_ramo] = 0.0 + return True + + +# --------------------------------------------------------------------------- +# 3. Gestor de crescimento — Hutchinson (correção do defect 1) + LPT +# --------------------------------------------------------------------------- + + +class GestorCrescimento: + """Cresce/poda microunidades por curvatura e uso (doc 11 Teorema 11.5, + Lema 11.1 corrigido). Sonda: Hutchinson–Rademacher E[v⊙Hv] = diag(H) + sobre os parâmetros DOS COMPOSITORES apenas (custo ~2 backwards extra, + a cada N passos).""" + + def __init__(self, compositores: list, cfg: dict, hub=None): + self.compositores = compositores + self.cfg = dict(cfg or {}) + self.hub = hub + self.tau_expand = float(self.cfg.get("tau_expand", 0.10)) + self.tau_prune = float(self.cfg.get("tau_prune", 0.01)) + self.eps_gate = float(self.cfg.get("eps_gate", 0.05)) + self.janela_uso = int(self.cfg.get("janela_uso", 8)) + self.n_max_ramos = int(self.cfg.get("n_max_ramos", 5)) + self.n_sondas = int(self.cfg.get("n_sondas", 2)) + self.ultimo_evento: dict = {} + self._uso_baixo: dict[int, deque] = {id(c): deque(maxlen=self.janela_uso) + for c in compositores} + self.eventos: list[dict] = [] + + # ---- LPT (Proposição 11.3): ordem de execução dos ramos ---- + @staticmethod + def ordem_lpt(custos: list[float], n_unidades: int = 2) -> tuple[list[int], float]: + """Retorna (ordem dos ramos, makespan estimado). LPT com min-heap: + makespan ≤ (4/3)·ÓTIMO (Graham 1969).""" + heap = [(0.0, u) for u in range(max(1, n_unidades))] + heapq.heapify(heap) + cargas = [0.0] * max(1, n_unidades) + ordem = [] + for idx in sorted(range(len(custos)), key=lambda i: -custos[i]): + carga, u = heapq.heappop(heap) + cargas[u] += custos[idx] + ordem.append(idx) + heapq.heappush(heap, (cargas[u], u)) + return ordem, max(cargas) if cargas else 0.0 + + # ---- Hutchinson–Rademacher da diagonal do Hessiano (Lema 11.1) ---- + def _diag_hessiano(self, perda_fn) -> tuple[dict, float]: + """E[v⊙Hv] = diag(H) por PARÂMETRO (tensor). Média sobre n_sondas + sondas Rademacher reduz a variância por 1/n_sondas.""" + compositores = [c for c in self.compositores if c.ramos] + if not compositores: + return {}, 0.0 + params = [p for c in compositores for p in c.ramos.parameters()] + # proprietário (índice do compositor/ramo) de cada parâmetro + dono: list[tuple[int, int]] = [] + for ci, c in enumerate(compositores): + for ri, u in enumerate(c.ramos): + for _ in u.parameters(): + dono.append((ci, ri)) + perda = perda_fn() + g = torch.autograd.grad(perda, params, create_graph=True, + allow_unused=True) + diag: dict[int, float] = {} + total = 0.0 + for _ in range(max(1, self.n_sondas)): + vs = [torch.randn_like(p).sign() for p in params] + gv = sum((gi * vi).sum() for gi, vi in zip(g, vs) + if gi is not None) + if not isinstance(gv, torch.Tensor): + break + # retain_graph: o MESMO grafo de g serve às n_sondas sondas + hv = torch.autograd.grad(gv, params, allow_unused=True, + retain_graph=True) + for i, p in enumerate(params): + if hv[i] is None or g[i] is None: + continue + contrib = float((vs[i] * hv[i]).sum().item()) + diag[i] = diag.get(i, 0.0) + contrib / max(1, self.n_sondas) + total += abs(contrib) / max(1, self.n_sondas) + # agrega por (compositor, ramo) — usado pela poda (Teorema 11.5) + por_ramo: dict[tuple[int, int], float] = {} + for i, valor in diag.items(): + chave = dono[i] + por_ramo[chave] = por_ramo.get(chave, 0.0) + valor + # libera o grafo de segunda ordem (RAM: probe + PCGrad no mesmo processo) + del g, diag + import gc + gc.collect() + return por_ramo, total + + def verificar(self, passo: int, perda_fn) -> dict: + """Expande (curvatura alta) / poda (uso baixo + curvatura baixa).""" + por_ramo, total = self._diag_hessiano(perda_fn) + resultado: dict = {"passo": passo, "curvatura_total": round(total, 6), + "acoes": []} + for ci, comp in enumerate(self.compositores): + uso = comp.uso_ema + if uso is None or uso.numel() < 2: + continue + uso_cpu = uso.detach().cpu() + ativos = [k for k in range(len(comp.nome_ramos)) + if float(comp.ramo_ativo[k]) > 0.5 + and comp.nome_ramos[k] != "moe"] + if not ativos: + continue + k_min = min(ativos, key=lambda k: float(uso_cpu[k])) + uso_min = float(uso_cpu[k_min]) + self._uso_baixo[id(comp)].append(uso_min < self.eps_gate) + # curvatura (soma da diagonal) dos parâmetros do ramo k_min + curv_ramo = por_ramo.get((ci, k_min), 0.0) + if total > self.tau_expand and len(comp.nome_ramos) < self.n_max_ramos: + tipo = "conv" if (ci % 2 == 0) else "rot" + comp.expandir(tipo) + acao = f"expandir[{tipo}]" + elif (sum(self._uso_baixo[id(comp)]) >= self.janela_uso + and abs(curv_ramo) < self.tau_prune): + if comp.podar(k_min): + acao = f"podar[{comp.nome_ramos[k_min]}]" + else: + acao = "poda_bloqueada" + else: + acao = "manter" + resultado["acoes"].append({"compositor": ci, "acao": acao, + "uso_min": round(uso_min, 4), + "curvatura_ramo": round(curv_ramo, 6)}) + if acao not in ("manter",) and self.hub is not None: + self.hub.atributo(f"crescimento/c{ci}_{acao}", 1.0, passo) + self.eventos.append({"passo": passo, "compositor": ci, + "acao": acao}) + self.ultimo_evento = resultado + return resultado diff --git a/src/khtst/percepcao/moe.py b/src/khtst/percepcao/moe.py new file mode 100644 index 0000000000000000000000000000000000000000..5b2ba0dccfe4a66b84d765c509af07d052816383 --- /dev/null +++ b/src/khtst/percepcao/moe.py @@ -0,0 +1,499 @@ +# -*- coding: utf-8 -*- +"""MoE Agrupável com Foco na Tarefa (doc 10 §1) — KHTST v2, item (a) do escopo. + +Ideia central: especialistas agrupados por família de tarefa (texto/visual/áudio), +com gate duplo s = ⟨x̄, μ_e⟩/√d + b_{g(e), τ} (eq. 10.1): a âncora μ_e mede +afinidade (indexação por protótipo — compactação), o viés b dá ênfase ao grupo +relevante para a tarefa corrente. Na fase densa TODOS os experts participam +(máximo de conexões); na fase foco só o top-k é computado — experts irrelevantes +são ignorados sem custo de forward (Proposição 10.4), permanecendo apenas como +linhas no índice. + +Perdas auxiliares: balanceamento agrupado L_lb (Teorema 10.2) e ortogonalidade +intra-grupo L_ort (Teorema 10.3) — especialistas do mesmo grupo são empurrados a +respostas complementares. +""" +from __future__ import annotations + +import torch +import torch.nn as nn +import torch.nn.functional as F + +# grupos canônicos por tarefa (famílias do escopo v2) +GRUPOS_TAREFA = { + "lm": 0, "noticia": 0, "pontuacao": 0, "instrucao": 0, "tts": 0, + "vqa": 1, "ocr": 1, "imagem_caption": 1, + "asr": 2, +} +NOMES_GRUPOS = ("texto", "visual", "audio") + + +class MLPExpert(nn.Module): + """Expert SwiGLU compacto (d → d_ff_e → d).""" + + def __init__(self, d: int, d_ff: int): + super().__init__() + self.w_gate = nn.Linear(d, d_ff, bias=False) + self.w_up = nn.Linear(d, d_ff, bias=False) + self.w_down = nn.Linear(d_ff, d, bias=False) + + def forward(self, x): + return self.w_down(F.silu(self.w_gate(x)) * self.w_up(x)) + + +# --------------------------------------------------------------------------- +# v8 (item e) — especialistas VETORIZADOS: um único kernel batched (vmap/bmm) +# substitui o laço Python sobre N experts. Teorema 19.11: +# • FLOPs IDÊNTICOS (mesma soma de GEMMs); wall-clock ↓ por (N−1)·c_lanç, +# onde c_lanç = overhead de lançamento de kernel (~5–10 μs); +# • erro numérico ≤ 1 ulp·N (soma comutativa em fp32 — mesma ordem efetiva); +# • torch.sum/torch.mean: reduções nativas (map-reduce) — sem laços Python. +# --------------------------------------------------------------------------- + + +def experts_vetoriais(x: torch.Tensor, w_gate: torch.Tensor, w_up: torch.Tensor, + w_down: torch.Tensor) -> torch.Tensor: + """x: (BT, d); w_*: (N, ...) → saída (N, BT, d) — TODOS os experts num + único passe vetorial (torch.vmap sobre a dimensão de expert).""" + def um_expert(xe, wg, wu, wd): + return F.linear(F.silu(F.linear(xe, wg)) * F.linear(xe, wu), wd) + try: + return torch.vmap(um_expert)(x.unsqueeze(0).expand(w_gate.shape[0], -1, -1), + w_gate, w_up, w_down) + except Exception: + # fallback: einsum equivale matematicamente (Teorema 19.11 — mesmo + # conjunto de produtos matriciais; comutatividade da soma) + h = F.silu(torch.einsum("td,nef->tnf", x, w_gate.transpose(-2, -1))) \ + * torch.einsum("td,nef->tnf", x, w_up.transpose(-2, -1)) + return torch.einsum("tnf,ndf->tnd", h, w_down.transpose(-2, -1)) + + +class MoEAgrupavel(nn.Module): + """Mixture of Experts agrupável por família de tarefa (doc 10 §1). + + Args: + d: dimensão do modelo. + n_grupos: G grupos (default 3 = texto/visual/áudio). + experts_por_grupo: E_g experts por grupo (N = G·E_g). + d_ff_expert: largura de cada expert. + top_k: k da fase foco. + n_tarefas: nº de tarefas conhecidas (tabela de viés G×n_tarefas). + """ + + def __init__(self, d: int, n_grupos: int = 3, experts_por_grupo: int = 2, + d_ff_expert: int = 192, top_k: int = 2, n_tarefas: int = 9, + margem_foco: float = 1.0, vetorial: bool = True): + super().__init__() + assert n_grupos >= 1 and experts_por_grupo >= 1 + self.d, self.G, self.Eg = d, n_grupos, experts_por_grupo + self.N = n_grupos * experts_por_grupo + self.top_k = top_k + self.margem_foco = margem_foco + self.vetorial = bool(vetorial) # v8 (item e) + self.especialistas = nn.ModuleList( + [MLPExpert(d, d_ff_expert) for _ in range(self.N)]) + self.grupo_de = torch.tensor([g for g in range(n_grupos) + for _ in range(experts_por_grupo)]) + self.ancoras = nn.Parameter(torch.randn(self.N, d) * 0.02) # μ_e (índice) + self.vies_tarefa = nn.Parameter(torch.zeros(n_grupos, n_tarefas)) + self.register_buffer("grupo_de_idx", self.grupo_de.clone()) + self.fase_densa = True # item (d): inicia com máximo de conexões + # v5 (doc 16 §§1–2): atenção ENTRE camadas MoE + retroalimentação de rotas + # — nasce NEUTRA (α=β=0 ⇒ v4 exato; Teorema 16.1c: não-regressão) + self.refino = None # RefinamentoEntreMoEs (ligado pelo modelo) + self.moe_anterior = None # camada MoE ℓ−1 + self.beta_feedback = nn.Parameter(torch.zeros(())) + self._Z_ultimo: torch.Tensor | None = None # (B*T, N, d) experts do último forward + self._g_ultimo: torch.Tensor | None = None # (N,) gate médio (router memory) + self.ultima_entropia_atn: float = 0.0 + # v6 (doc 18 §1): ROTEAMENTO POR S-SOM — roteador externo (compartilhado); + # nasce None ⇒ v5 exato (Teorema 18.1: nascimento neutro) + self.roteador = None + self.ultima_frac_rotas = 0.0 + # v8 (item e): execução VETORIAL dos experts (vmap/bmm — Teorema 19.11); + # math é idêntica ao laço original (comutatividade da soma ponderada) + self.vetorial = bool(vetorial) + # telemetria do último forward + self.ultimo_f: torch.Tensor | None = None # fração roteada (B,N)→média + self.ultimo_p: torch.Tensor | None = None # gate médio + self.perda_lb: torch.Tensor = torch.zeros(()) + self.perda_ort: torch.Tensor = torch.zeros(()) + self.uso_ema: torch.Tensor | None = None # EMA de ativação (N,) + self.ultimo_ignorados: int = 0 + + # ---------------- utilidades ---------------- + def indice_grupo(self, tarefa: str | None) -> int: + if tarefa is None: + return 0 + return GRUPOS_TAREFA.get(tarefa, 0) + + def _ort_intra_grupo(self, ativações: torch.Tensor) -> torch.Tensor: + """ativações: (B, N) médias normalizadas → L_ort por grupos (Teorema 10.3).""" + norma = ativações / (ativações.norm(dim=0, keepdim=True) + 1e-8) # (B,N) + perda = ativações.new_zeros(()) + for g in range(self.G): + idx = (self.grupo_de_idx == g).nonzero(as_tuple=True)[0] + if idx.numel() < 2: + continue + Phi = norma[:, idx].T @ norma[:, idx] # (Eg, Eg) + tr = Phi.pow(2).sum() - Phi.diagonal().pow(2).sum() + perda = perda + tr / (idx.numel() * (idx.numel() - 1)) + return perda / max(self.G, 1) + + # ---------------- forward ---------------- + def forward(self, x: torch.Tensor, tarefa: str | None = None) -> torch.Tensor: + """x: (B, T, d) → saída (B, T, d) + perdas auxiliares atualizadas.""" + B, T, d = x.shape + x_flat = x.reshape(B * T, d) + # v3 (doc 11 §12): roteamento POR TOKEN e CAUSAL — cada posição é + # roteada pelo seu próprio estado (na decodificação T=1 é o estado + # corrente; no forward paralelo de treino NÃO vê o futuro). Isto + # substitui a compactação x̄ por amostra da v2, que vazava contexto + # futuro no roteamento das posições passadas. + # gate duplo (eq. 10.1): afinidade por âncora + viés de grupo-tarefa + it = self.indice_grupo(tarefa) + s = (x_flat @ self.ancoras.T) / (self.d ** 0.5) # (B*T, N) + # viés b_{g(e),τ}: cada expert recebe o viés do SEU grupo para τ + vies_experts = self.vies_tarefa[:, it][self.grupo_de_idx] # (N,) + s = s + vies_experts.unsqueeze(0) + # v5 — eq. 16.2: retroalimentação do padrão de rotas da camada ℓ−1 + # (contracção garantida — Teorema 16.2; β=0 no nascimento) + if self.moe_anterior is not None and self.moe_anterior._g_ultimo is not None: + s = s + self.beta_feedback * self.moe_anterior._g_ultimo.unsqueeze(0).to(s.dtype) + # v6 — eq. 18.1: roteamento por S-SOM (viés condicional ao contexto; + # zero em empates de Voronoi, zero antes do nascimento neutro) + if self.roteador is not None and self.roteador.pronto: + z_med = x.mean(dim=1) # (B, d) + vies_g, frac = self.roteador.vies_grupo(z_med) # (B, G) + self.ultima_frac_rotas = frac + if float(vies_g.abs().sum()) > 0.0: + vies_e = vies_g[:, self.grupo_de_idx].unsqueeze(1) # (B, 1, N) + s = s.reshape(B, T, -1) + vies_e.to(s.dtype) + s = s.reshape(B * T, -1) + if self.fase_densa: + # fase densa: softmax sobre TODOS os experts (máximo de conexões) + g = torch.softmax(s, dim=-1) # (B*T, N) + mask_ativos = torch.ones_like(g, dtype=torch.bool) + else: + # fase foco: top-k dentro do grupo de τ + extras elegíveis por margem + idx_grupo = (self.grupo_de_idx == it).nonzero(as_tuple=True)[0] + fora = (self.grupo_de_idx != it).nonzero(as_tuple=True)[0] + s_grupo = s[:, idx_grupo] + k = min(self.top_k, idx_grupo.numel()) + topo_val, topo_idx = s_grupo.topk(k, dim=-1) # (B*T, k) + corte = topo_val[:, -1:].detach() # s_(k) + extras = fora[(s[:, fora] > corte - self.margem_foco).any(dim=0)] \ + if fora.numel() else fora + ativos = torch.cat([idx_grupo, extras]) if extras.numel() else idx_grupo + s_at = s[:, ativos] + g_at = torch.softmax(s_at, dim=-1) # sobre ativos + g = torch.zeros_like(s) + g[:, ativos] = g_at + mask_ativos = torch.zeros_like(s, dtype=torch.bool) + mask_ativos[:, ativos] = True + self.ultimo_ignorados = int((~mask_ativos).sum(dim=1).float().mean()) + # v8 (item e): computa APENAS os experts ativos num único passe vetorial + # (vmap/bmm — Teorema 19.11); na fase densa N = todos. Equivale ao laço + # original (Prop. 10.4 preservada por indexação de pesos, não por laço). + # Views empilhadas: gradientes fluem aos parâmetros originais (stack é + # diferenciável) — state_dict/DPO/clone preservados. + with torch.no_grad(): + idx_at = mask_ativos.any(dim=0).nonzero(as_tuple=True)[0] # (A,) + w = g.index_select(1, idx_at) # (BT, A) + w_gate = torch.stack([self.especialistas[i].w_gate.weight + for i in idx_at.tolist()]) + w_up = torch.stack([self.especialistas[i].w_up.weight + for i in idx_at.tolist()]) + w_down = torch.stack([self.especialistas[i].w_down.weight + for i in idx_at.tolist()]) + YE = experts_vetoriais(x_flat, w_gate, w_up, w_down) # (A, BT, d) + y = torch.sum(w.T.unsqueeze(-1) * YE, dim=0) # Σ_e g_e·y_e (map-reduce) + soma_at = w.sum(dim=1, keepdim=True) # (BT, 1) + acts = [YE[a_idx] for a_idx in range(YE.shape[0])] # ativos primeiro + # reconstrói a lista completa (zeros nos inativos) para L_ort/telemetria + acts_full = [] + pos = 0 + for e in range(self.N): + if pos < idx_at.numel() and int(idx_at[pos]) == e: + acts_full.append(acts[pos]) + pos += 1 + else: + acts_full.append(torch.zeros(B * T, d, device=x.device, dtype=x.dtype)) + acts = acts_full + # normalização quando fase foco (soma dos gates ativos = 1 por amostra) + y = y / soma_at.clamp(min=1e-6) + # v5 — eq. 16.1: refinamento por atenção sobre os experts da camada ℓ−1 + self._Z_ultimo = torch.stack(acts, dim=1) # (B*T, N, d) + self._g_ultimo = g.mean(dim=0).detach() # router memory + if self.refino is not None and self.moe_anterior is not None \ + and self.moe_anterior._Z_ultimo is not None: + z_prev = self.moe_anterior._Z_ultimo + if z_prev.shape[0] == y.shape[0]: + y_3d = y.reshape(B, T, d) + y_3d = self.refino(y_3d, z_prev.reshape(B, T, self.N, d)) + self.ultima_entropia_atn = self.refino.entropia_media( + y_3d.detach(), z_prev.reshape(B, T, self.N, d).detach()) + y = y_3d.reshape(B * T, d) + # telemetria e perdas auxiliares + with torch.no_grad(): + p_medio = g.mean(dim=0) # (N,) + f_medio = (g > 1.0 / self.N).float().mean(dim=0) + self.ultimo_p = p_medio.detach() + self.ultimo_f = f_medio.detach() + if self.uso_ema is None: + self.uso_ema = p_medio.detach().clone() + else: + self.uso_ema = 0.95 * self.uso_ema + 0.05 * p_medio.detach() + # L_lb agrupado (eq. 10.3) — diferenciável pelo gate p + lb = x.new_zeros(()) + for gi in range(self.G): + idx = (self.grupo_de_idx == gi).nonzero(as_tuple=True)[0] + if idx.numel() == 0: + continue + fg = g[:, idx].mean(dim=0) + pg = g[:, idx].mean(dim=0) + lb = lb + (fg * pg).sum() + self.perda_lb = lb / self.G + # L_ort: ativações médias por expert (do MESMO forward — sem recomputar) + A = torch.stack([a.mean(dim=0) for a in acts], dim=1) # (d, N) + self.perda_ort = self._ort_intra_grupo(A) + return y.reshape(B, T, d) + + # ---------------- métricas ---------------- + def estatisticas(self) -> dict: + out = {"fase": "densa" if self.fase_densa else "foco", + "n_experts": self.N, "grupos": self.G, "top_k": self.top_k} + if self.uso_ema is not None: + out["uso_max"] = float(self.uso_ema.max()) + out["uso_min"] = float(self.uso_ema.min()) + out["experts_domintes"] = int((self.uso_ema > 2.0 / self.N).sum()) + # v5 — atenção entre MoEs (doc 16 §2) + out["beta_feedback"] = float(self.beta_feedback) + out["entropia_atn_entre_moe"] = self.ultima_entropia_atn + if self.refino is not None: + out["alpha_refino"] = float(self.refino.alpha) + # v6 — roteamento por S-SOM (doc 18 §1) + out["frac_rotas_ssom"] = self.ultima_frac_rotas + if self.roteador is not None: + out["roteador_pronto"] = bool(self.roteador.pronto) + out["roteador_amostras"] = self.roteador.n_amostras + return out + + +# --------------------------------------------------------------------------- +# v8 (item g) — MoE ENCODER-DECODER FORA DE ORDEM agrupável/desagrupável +# --------------------------------------------------------------------------- + + +class MoEEncoderDecoderKHTST(nn.Module): + """MoE hierárquica com 2 ENCODERS e 4 DECODERS fora de ordem (doc 19 §6). + + Arquitetura (requisito explícito: DOIS encoders e QUATRO decoders): + 1) ESTÁGIO ENCODER: 2 experts codificam cada token (top-1 por gate de + afinidade + viés S-SOM) → z; + 2) ESTÁGIO DECODER: 4 experts especializados por grupo de tarefa + processam z (top-1 por gate + viés do roteador S-SOM) → y. + + FORA DE ORDEM (Teorema 19.12): os DECODERS são executados numa permutação + π_t determinística por passo (π_t = (π_0 + t) mod 4) e os TOKENS são + reagrupados por expert (bucketing/dispacho disperso). Como cada expert é + uma função token-a-token e a agregação é SOMA PONDERADA (comutativa), a + saída é INVARIANTE à ordem de execução: y_π = y_id para todo π. O ganho é + de LOCALIDADE (batches densos por expert) e de balanceamento dinâmico. + + AGRUPÁVEL/DESAGRUPÁVEL (Teorema 19.13): `agrupar()` funde os 4 decoders + num único expert denso por média ponderada de uso (função idêntica no + nascimento: média de experts idênticos ⇒ saída igual); `desagrupar()` + devolve os 4 experts com os pesos preservados (continuidade exata). + """ + + def __init__(self, d: int, n_encoders: int = 2, n_decoders: int = 4, + d_ff_expert: int = 160, top_k: int = 2, + fora_de_ordem: bool = True): + super().__init__() + self.d = d + self.n_enc, self.n_dec = n_encoders, n_decoders + self.top_k = max(1, min(top_k, n_decoders)) + self.fora_de_ordem = bool(fora_de_ordem) + self.agrupado = False + # v8.1 — fase DENSA (doc 10 §1.1): todos os experts com softmax total + # (máximo de conexões no início do treino); foco → top-k + self.fase_densa = True + self.encoders = nn.ModuleList([MLPExpert(d, d_ff_expert) + for _ in range(n_encoders)]) + self.decoders = nn.ModuleList([MLPExpert(d, d_ff_expert) + for _ in range(n_decoders)]) + # gates: afinidade por protótipo (ancoras) — compactação por indexação + self.ancoras_enc = nn.Parameter(torch.randn(n_encoders, d) * 0.02) + self.ancoras_dec = nn.Parameter(torch.randn(n_decoders, d) * 0.02) + # retroalimentação S-SOM (nasce neutra — Teorema 18.1) + self.roteador = None + self.passo_interno = 0 + # compat v5/v6 (RefinamentoEntreMoEs + telemetria do modelo): + # a enc-dec não participa do refinamento entre MoEs (nasce neutra) + self.refino = None + self.moe_anterior = None + self.beta_feedback = nn.Parameter(torch.zeros(())) + self._Z_ultimo: torch.Tensor | None = None + self._g_ultimo: torch.Tensor | None = None + self.ultima_entropia_atn: float = 0.0 + self.uso_ema: torch.Tensor | None = None + self.ultimo_f: torch.Tensor | None = None + self.ultimo_p: torch.Tensor | None = None + # telemetria própria + self.uso_enc_ema = torch.zeros(n_encoders) + self.uso_dec_ema = torch.zeros(n_decoders) + self.ultima_frac_rotas = 0.0 + self.ultimo_ignorados = 0 + self.perda_lb = torch.zeros(()) + self.perda_ort = torch.zeros(()) + + @property + def N(self) -> int: + """Total de experts (compat telemetria/RPP): 2 encoders + 4 decoders.""" + return self.n_enc + self.n_dec + + # ---------------- agrupável / desagrupável ---------------- + @torch.no_grad() + def agrupar(self): + """Funde os decoders num expert denso (modo agrupado). Teorema 19.13: + com pesos θ_i iguais à média ponderada de uso, a função fundida + coincide com a média das funções — CONTINUIDADE na troca.""" + if self.agrupado: + return + w = self.uso_dec_ema / (self.uso_dec_ema.sum() + 1e-8) + self.decoder_fundido = MLPExpert(self.d, self.decoders[0].w_gate.out_features) + for alvo, src in ((self.decoder_fundido.w_gate, "w_gate"), + (self.decoder_fundido.w_up, "w_up"), + (self.decoder_fundido.w_down, "w_down")): + alvo.weight.zero_() + for i, dec in enumerate(self.decoders): + alvo.weight.add_(getattr(dec, src).weight, alpha=float(w[i])) + self.agrupado = True + + @torch.no_grad() + def desagrupar(self): + """Restaura os 4 decoders com os pesos originais (continuidade exata).""" + self.agrupado = False + + # ---------------- forward ---------------- + @torch.no_grad() + def _gates(self, x_flat: torch.Tensor, ancoras: torch.Tensor) -> torch.Tensor: + s = (x_flat @ ancoras.T) / (self.d ** 0.5) + return s + + def _executa_experts(self, x_flat: torch.Tensor, experts: nn.ModuleList, + s: torch.Tensor, k: int, + uso_ema: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """Fase densa: softmax total (k=None). Foco: top-k fora de ordem com + reagrupamento por expert (bucketing). Devolve (y (BT,d), g (BT,E)).""" + BT, d = x_flat.shape + E = len(experts) + if self.fase_densa and not self.agrupado: + g = torch.softmax(s, dim=-1) + # fase densa FORA DE ORDEM: soma comutativa ⇒ ordem livre (Teo 19.12) + ordem = list(range(E)) + if self.fora_de_ordem and E > 1: + desloc = self.passo_interno % E + ordem = ordem[desloc:] + ordem[:desloc] + y = torch.zeros(BT, d, device=x_flat.device, dtype=x_flat.dtype) + for e in ordem: + y.index_add_(0, torch.arange(BT, device=x_flat.device), + g[:, e].unsqueeze(1) * experts[e](x_flat)) + return y, g + if self.agrupado and E > 1: + experts = [experts[0]] + E = 1 + g = torch.softmax(s, dim=-1) + y = experts[0](x_flat) + return y, g + # top-k por token + topo_val, topo_idx = s.topk(k, dim=-1) # (BT, k) + g = torch.zeros_like(s) + g.scatter_(-1, topo_idx, F.softmax(topo_val, dim=-1)) + # FORA DE ORDEM: ordem de execução dos experts = permutação cíclica + ordem = list(range(E)) + if self.fora_de_ordem and E > 1: + desloc = self.passo_interno % E + ordem = ordem[desloc:] + ordem[:desloc] + y = torch.zeros(BT, d, device=x_flat.device, dtype=x_flat.dtype) + # bucketing: tokens agrupados por expert denso (localidade de cache) + for e in ordem: + mask_e = (topo_idx == e) # (BT, k) + if not bool(mask_e.any()): + continue + linhas = mask_e.any(dim=1).nonzero(as_tuple=True)[0] + x_e = x_flat.index_select(0, linhas) + y_e = experts[e](x_e) + w_e = (g[linhas, e]).unsqueeze(1) + y.index_add_(0, linhas, w_e * y_e) + return y, g + + def forward(self, x: torch.Tensor, tarefa: str | None = None) -> torch.Tensor: + B, T, d = x.shape + x_flat = x.reshape(B * T, d) + self.passo_interno += 1 + # viés do roteador S-SOM (nascimento neutro — Teorema 18.1) + vies = 0.0 + if self.roteador is not None and getattr(self.roteador, "pronto", False): + z_med = x.mean(dim=1) + vies_g, frac = self.roteador.vies_grupo(z_med) + self.ultima_frac_rotas = frac + if float(vies_g.abs().sum()) > 0.0: + # primeiro grupo do lote determina o deslocamento fora-de-ordem + vies = float(vies_g.abs().max()) + # estágio encoder (densa: ambos; foco: top-1) + s_enc = self._gates(x_flat, self.ancoras_enc) + y_enc, g_enc = self._executa_experts(x_flat, self.encoders, s_enc, + None if self.fase_densa else 1, + self.uso_enc_ema) + # estágio decoder (top-k) sobre a codificação + s_dec = self._gates(y_enc, self.ancoras_dec) + if vies: + s_dec = s_dec + vies + # v5/v8 — eq. 16.2 adaptada: retroalimentação do padrão de rotas da + # camada ℓ−1. Como a enc-dec tem N_dec experts (≠ N_{ℓ−1}), o vetor + # de rotas é RE-SUMARIZADO por pooling adaptativo (operador + # determinístico P: R^{N_prev} → R^{n_dec}); β=0 no nascimento + # ⇒ não-regressão (Teorema 16.1c preservado). + if self.moe_anterior is not None and self.moe_anterior._g_ultimo is not None: + g_prev = self.moe_anterior._g_ultimo.to(s_dec.dtype) + if g_prev.numel() != self.n_dec: + g_prev = F.adaptive_avg_pool1d( + g_prev.view(1, 1, -1), self.n_dec).view(-1) + s_dec = s_dec + self.beta_feedback * g_prev.unsqueeze(0) + y_dec, g_dec = self._executa_experts(y_enc, self.decoders, s_dec, + None if self.fase_densa else self.top_k, + self.uso_dec_ema) + self._g_ultimo = g_dec.mean(dim=0).detach() # router memory (doc 16) + # telemetria EMA + perda de balanceamento (Teorema 10.2 generalizada) + with torch.no_grad(): + f_enc = (g_enc > 1.0 / self.n_enc).float().mean(dim=0) + f_dec = (g_dec > 1.0 / self.n_dec).float().mean(dim=0) + p_enc = g_enc.mean(dim=0) + p_dec = g_dec.mean(dim=0) + self.uso_enc_ema = 0.95 * self.uso_enc_ema + 0.05 * p_enc + self.uso_dec_ema = 0.95 * self.uso_dec_ema + 0.05 * p_dec + # compat telemetria/script 05 (top-k por tarefa) + self.ultimo_p = p_dec.detach() + self.ultimo_f = f_dec.detach() + self.perda_lb = (self.n_enc * float((f_enc * p_enc).sum()) + + self.n_dec * float((f_dec * p_dec).sum())) + # v5 — eq. 16.1: REFINAMENTO por atenção sobre os experts da camada ℓ−1 + # (preservado na enc-dec — mantém a lógica do doc 16 funcional) + if self.refino is not None and self.moe_anterior is not None \ + and self.moe_anterior._Z_ultimo is not None: + z_prev = self.moe_anterior._Z_ultimo + if z_prev.shape[0] == y_dec.shape[0]: + y_3d = self.refino(y_dec.reshape(B, T, d), + z_prev.reshape(B, T, self.moe_anterior.N, d)) + self.ultima_entropia_atn = self.refino.entropia_media( + y_3d.detach(), z_prev.reshape(B, T, self.moe_anterior.N, d).detach()) + y_dec = y_3d.reshape(B * T, d) + return y_dec.reshape(B, T, d) + + def estatisticas(self) -> dict: + return {"n_encoders": self.n_enc, "n_decoders": self.n_dec, + "agrupado": self.agrupado, "fora_de_ordem": self.fora_de_ordem, + "uso_enc": self.uso_enc_ema.tolist(), + "uso_dec": self.uso_dec_ema.tolist()} diff --git a/src/khtst/percepcao/ortogonais.py b/src/khtst/percepcao/ortogonais.py new file mode 100644 index 0000000000000000000000000000000000000000..0686199415161b206a6815b47f0483cb7db04048 --- /dev/null +++ b/src/khtst/percepcao/ortogonais.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +"""Camadas otimizadas quanto ao esforço (item 6): + • CamadaOrtogonal — W mantido ortogonal (Teorema 9.1: Householder/QR); + • CamadaCooperativa — mistura de expertos com balanceamento (Teorema 9.3). + +Ortogonalidade preserva normas (κ(W)=1 ⇒ gradiente estável, Teorema 9.2) e é +verificável: a telemetria mede ‖WWᵀ−I‖_max; violação acima de 1e-3 é BUG. +""" +from __future__ import annotations + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class CamadaOrtogonal(nn.Module): + """Linear com projeção QR periódica: W ← QR(W).R_diag_normalizada. + + Implementação pragmática: chamamos `renormalizar()` a cada N passos do + treino (custo O(d²) amortizado) e o forward usa a última base ortogonal + em cache — esforço otimizado (nada de QR por passo). + """ + + def __init__(self, d_entrada: int, d_saida: int, vies: bool = True): + super().__init__() + self.w = nn.Parameter(torch.empty(d_saida, d_entrada)) + nn.init.orthogonal_(self.w) # inicialização ortogonal exata + self.b = nn.Parameter(torch.zeros(d_saida)) if vies else None + self._w_ort: torch.Tensor | None = None + + @torch.no_grad() + def renormalizar(self) -> float: + """QR de Gram-Schmidt via torch.linalg.qr; grava Q de volta em W + (ortogonalização real do peso) e devolve o desvio pré-projeção.""" + Q, _ = torch.linalg.qr(self.w.data) + self._w_ort = None + ww_velho = self.w.data @ self.w.data.T + desvio = float(torch.max(torch.abs(ww_velho - torch.eye(ww_velho.shape[0])))) + self.w.data.copy_(Q) # W ← Q : W Wᵀ = I exato (Teorema 9.1) + return desvio + + def forward(self, x: torch.Tensor) -> torch.Tensor: + w = self._w_ort if (self._w_ort is not None and not self.training) else self.w + y = F.linear(x, w, self.b) + return y + + def metrica_ortogonalidade(self) -> float: + with torch.no_grad(): + ww = self.w @ self.w.T + n = ww.shape[0] + return float(torch.max(torch.abs(ww - torch.eye(n)))) + + +class CamadaCooperativa(nn.Module): + """Mistura de N expertos lineares com gate; top-k=2 sempre ativos + (cooperação forçada) + perda de balanceamento L_bal = N Σ f_i P_i (eq. 9.2). + """ + + def __init__(self, d: int, n_expertos: int = 2, d_oculto: int | None = None): + super().__init__() + d_oculto = d_oculto or d + self.n = n_expertos + self.expertos = nn.ModuleList([ + nn.Sequential(nn.Linear(d, d_oculto), nn.SiLU(), nn.Linear(d_oculto, d)) + for _ in range(n_expertos)]) + self.gate = nn.Linear(d, n_expertos) + self.ultimo_f = torch.zeros(n_expertos) + self.ultimo_p = torch.zeros(n_expertos) + + def forward(self, x: torch.Tensor, treinar_balanceamento: bool = True): + logits = self.gate(x) # (B, T, N) + p = F.softmax(logits, dim=-1) + # top-2 expertos por token (cooperação: 2 ativos sempre) + topk = p.topk(min(2, self.n), dim=-1) + y = torch.zeros_like(x) + B, T, _ = x.shape + contagem = torch.zeros(self.n, device=x.device) + peso_total = torch.zeros(self.n, device=x.device) + for idx in range(topk.indices.shape[-1]): + ids = topk.indices[..., idx] # (B, T) + pesos = torch.gather(p, -1, ids.unsqueeze(-1)).squeeze(-1) + for e in range(self.n): + masc = (ids == e) + if masc.any(): + x_e = x[masc] + y_e = self.expertos[e](x_e) + y[masc] += pesos[masc].unsqueeze(-1) * y_e + contagem[e] += masc.sum() + peso_total[e] += pesos[masc].sum() + self.ultimo_f = (contagem / max(int(B * T), 1)).detach() + self.ultimo_p = (peso_total / max(int(B * T), 1)).detach() + if treinar_balanceamento: + lb = self.n * float((self.ultimo_f * self.ultimo_p).sum()) + # guardamos para o treinador somar à perda (Teorema 9.3: mínimo 1/N) + self.perda_balanceamento = lb + return y + + @property + def perda_balanceamento(self) -> torch.Tensor: + val = getattr(self, "_perda_bal", None) + if val is None: + val = self.n * (self.ultimo_f * self.ultimo_p).sum() + return val + + @perda_balanceamento.setter + def perda_balanceamento(self, v): + self._perda_bal = torch.as_tensor(v) diff --git a/src/khtst/percepcao/roteador_ssom.py b/src/khtst/percepcao/roteador_ssom.py new file mode 100644 index 0000000000000000000000000000000000000000..767f1fea34854d6cf80e67eee42f551ed33318db --- /dev/null +++ b/src/khtst/percepcao/roteador_ssom.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +"""ROTEAMENTO POR S-SOM (doc 18 §1) — o S-SOM supervisionado vira roteador. + +Treino incremental: células do S-SOM aprendem (z → τ) com z = contexto médio +do lote e τ = índice da tarefa (eq. 6.2). No forward do MoE, o viés de rota +(eq. 18.1) soma ao gate duplo: + + s_e ← s_e + λ_rota · [log p(τ|c*(z)) − log(1/C)] para e do grupo predito, + +com DUAS restrições de segurança (Teoremas 18.2 e 18.6): + • NASCIMENTO NEUTRO: λ_rota efetivo = 0 até min_amostras (não-regressão); + • RESTRIÇÃO A EMPATES DE VORONOI: se as duas células mais próximas quase + empatam (d²₂ ≤ d²₁(1+γ)), o roteador NÃO interfere (viés 0) — fora de + empates a célula dominante manda, preservando o Teorema 18.3. + +O viés é NORMALIZADO por log(1/C): nulo na distribuição uniforme, positivo +quando p(τ|c*) > 1/C — nunca negativo, nunca dominante (cota Teorema 18.2). +""" +from __future__ import annotations + +import torch + +from khtst.acelerado import som_atualizar_lote as _som_acelerado +from khtst.memoria.variantes_especiais import SupervisedSOM +from khtst.percepcao.moe import GRUPOS_TAREFA + +# ordem canônica das tarefas (9) — IDÊNTICA à KHTSTModel.TAREFAS +LISTA_TAREFAS = ("lm", "noticia", "pontuacao", "instrucao", "tts", + "vqa", "ocr", "imagem_caption", "asr") + + +class RoteadorSSOM: + """S-SOM como roteador de MoE (doc 18 §1).""" + + def __init__(self, dim: int, n_tarefas: int = 9, k: int = 24, + lambda_rota: float = 0.35, gamma_empate: float = 0.15, + conf_min: float = 0.25, min_amostras: int = 200, + semente: int = 2026): + self.ssom = SupervisedSOM(dim, k=k, n_classes=n_tarefas, + lambda_sup=0.5, semente=semente) + self.dim = dim + self.n_tarefas = n_tarefas + self.lambda_rota = lambda_rota + self.gamma_empate = gamma_empate + self.conf_min = conf_min # p(τ|c*) mínimo para interferir + self.min_amostras = min_amostras # nascimento neutro (Teorema 18.1) + self.n_amostras = 0 + # grupo canônico de cada tarefa (por LISTA_TAREFAS) + self.grupo_da_tarefa = torch.tensor( + [GRUPOS_TAREFA.get(t, 0) for t in LISTA_TAREFAS]) + # buffers: snapshots p/ drift + contadores + self.w_anterior: torch.Tensor | None = None + self.ultimo_drift: float = 0.0 + self.ultima_conf_media: float = 0.0 + self.ultima_frac_ativa: float = 0.0 # fração de lotes em que atuou + self.ultimos_acertos: int = 0 # tarefa real == grupo predito + + # ---------------- treino incremental ---------------- + @torch.no_grad() + def atualizar(self, z: torch.Tensor, idx_tarefas: torch.Tensor) -> float: + """z: (B, d) contextos médios; idx_tarefas: (B,) índices canônicos. + Um passo por amostra (Regra de Kohonen + CE supervisionada eq. 6.2). + v8: núcleo Cython+C (Teorema 19.25 — semântica sequencial exata, + speedup medido ~18×); fallback torch mantém a MESMA ordem de updates. + Retorna o EQ do lote (telemetria).""" + if z.dim() == 1: + z = z.unsqueeze(0) + z = z.float() + eq, self.n_amostras = _som_acelerado( + self.ssom.w, self.ssom.v, self.ssom.hit_ema, z, idx_tarefas, + 0.05, self.ssom.lambda_sup, self.n_amostras) + # drift do codebook do roteador (métrica de convergência) + if self.w_anterior is not None and self.w_anterior.shape == self.ssom.w.shape: + self.ultimo_drift = float((self.ssom.w - self.w_anterior).norm() + / (self.w_anterior.norm() + 1e-12)) + self.w_anterior = self.ssom.w.detach().clone() + return float(torch.cdist(z, self.ssom.w).pow(2).min(dim=1).values.mean()) + + # ---------------- consolidação supervisionada (fase SOM) ---------------- + @torch.no_grad() + def consolidar(self, z: torch.Tensor, rotulos: torch.Tensor, + passos: int = 6) -> float: + """Consolidação do roteador na fase SOM (doc 18 §1.1): múltiplos passes + de Kohonen + CE, RESEEDING de células mortas com amostras reais (mesmo + contrato das 8 variantes — Teorema 15.2) e v-heads por CONTAGENS + EMPÍRICAS Laplace-suavizadas: p(τ|c*) = (n_{c,τ}+α)/(n_c+α·C) — + estimador de frequência com margem (Teorema 18.3). Retorna o EQ.""" + z = z.float() + if z.dim() == 1: + z = z.unsqueeze(0) + for _ in range(passos): + for i in torch.randperm(z.shape[0]).tolist(): + self.n_amostras += 1 + xi = z[i] + c = int(torch.cdist(xi.unsqueeze(0), self.ssom.w).argmin()) + eta = 0.05 / (1.0 + 0.001 * self.n_amostras) + self.ssom.w[c] += eta * (xi - self.ssom.w[c]) + self.ssom.hit_ema *= 0.92 + self.ssom.hit_ema[c] += 0.08 + # reseeding: células mortas recebem amostras reais (anti-colapso) + mortos = (self.ssom.hit_ema < self.ssom.eps_ativo).nonzero(as_tuple=True)[0] + if mortos.numel() and z.shape[0] > 0: + fontes = z[torch.randint(0, z.shape[0], (int(mortos.numel()),))] + self.ssom.w[mortos] = fontes + self.ssom.hit_ema[mortos] = 0.08 + self.ssom.erro_ema[mortos] = 0.0 + # v-heads: frequência empírica suavizada (softmax(log p) = p) + c_all = torch.cdist(z, self.ssom.w).argmin(dim=1) + cont = torch.zeros_like(self.ssom.v) + for ci, yi in zip(c_all.tolist(), rotulos.tolist()): + cont[ci, min(int(yi), self.n_tarefas - 1)] += 1.0 + alfa = 0.5 + p = (cont + alfa) / (cont.sum(dim=1, keepdim=True) + alfa * self.n_tarefas) + self.ssom.v = torch.log(p.clamp(min=1e-9)) + self.w_anterior = self.ssom.w.detach().clone() + # telemetria fresca: sonda do próprio conjunto (confiança/rotas) + self.vies_grupo(z[:min(32, z.shape[0])]) + return float(torch.cdist(z, self.ssom.w).pow(2).min(dim=1).values.mean()) + + # ---------------- consulta ---------------- + @property + def pronto(self) -> bool: + return self.n_amostras >= self.min_amostras + + @torch.no_grad() + def vies_grupo(self, z: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """z: (B, d) → (viés (B, N_grupos), fração_ativa). + Viés = λ·[log p(τ|c*) − log(1/C)] NO grupo predito; 0 nas demais + colunas; 0 para TODAS as amostras em empate de Voronoi ou com + confiança < conf_min ou antes de `pronto` (Teoremas 18.1/18.2/18.6).""" + B = z.shape[0] + G = int(self.grupo_da_tarefa.max()) + 1 + vies = torch.zeros(B, G) + if not self.pronto: + return vies, 0.0 + z = z.float() + d2 = torch.cdist(z, self.ssom.w).pow(2) # (B, K) + d2_min, c = d2.min(dim=1) + d2_seg = d2.topk(2, dim=1, largest=False).values[:, 1] + # RESTRIÇÃO A EMPATES: 2º BMU dentro de (1+γ)·d²_min ⇒ não interfere + empate = d2_seg <= d2_min * (1.0 + self.gamma_empate) + probs = torch.softmax(self.ssom.v[c], dim=1) # (B, C) + conf, tau = probs.max(dim=1) # (B,) + elegivel = (~empate) & (conf >= self.conf_min) + if elegivel.any(): + grupos = self.grupo_da_tarefa[tau[elegivel]] + bonus = self.lambda_rota * \ + (torch.log(conf[elegivel].clamp(min=1e-6)) + + torch.log(torch.tensor(float(self.n_tarefas)))) + bonus = bonus.clamp(min=0.0, + max=self.lambda_rota * + torch.log(torch.tensor(float(self.n_tarefas * 1e3)))) + linhas = elegivel.nonzero(as_tuple=True)[0] + vies[linhas, grupos] = bonus + self.ultimos_acertos = int(elegivel.sum()) + self.ultima_frac_ativa = float(elegivel.float().mean()) + self.ultima_conf_media = float(conf.mean()) + return vies, self.ultima_frac_ativa + + # ---------------- telemetria ---------------- + def estatisticas(self) -> dict: + return {"amostras": self.n_amostras, "pronto": self.pronto, + "lambda_rota": self.lambda_rota, + "gamma_empate": self.gamma_empate, + "drift_codebook": round(self.ultimo_drift, 6), + "conf_media": round(self.ultima_conf_media, 4), + "frac_rotas_ativas": round(self.ultima_frac_ativa, 4), + "k": int(self.ssom.w.shape[0]), + "taxa_ativos": (round(float((self.ssom.hit_ema >= + self.ssom.eps_ativo) + .float().mean()), 4))} diff --git a/src/khtst/percepcao/video.py b/src/khtst/percepcao/video.py new file mode 100644 index 0000000000000000000000000000000000000000..7b48bb87d69db323f9171cea41aebf8aa9c43855 --- /dev/null +++ b/src/khtst/percepcao/video.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +"""Encoder de vídeo — amostra N frames → ViT compacto por frame → fusão temporal +por atenção (query = [CLS] do frame médio) → embedding d_modelo. + +v8 (KHTST — evolução ViT do requisito): + • ToMe (Token Merging) entre os blocos ViT de cada frame — COMPRESSÃO de + tokens com auto-ajuste da fração r (Teorema 19.22); + • ANALISADOR DE LUZ LRA-QViT (vit_lra.py): ramificações 1-bit ∥ 4-bit em + paralelo detectam a DIREÇÃO VETORIAL da iluminação e a INTENSIDADE das + fontes + mapas 2-D de contorno/área; o vetor de luz entra no embedding + por projeção NASCIDA NEUTRA (γ=0 — não-regressão, Teorema 19.4). + +Compartilha o estilo do CodificadorImagem mas com dimensão menor (config +video.d) — o custo é linear no nº de frames amostrados. +""" +from __future__ import annotations + +import torch +import torch.nn as nn + +from khtst.percepcao.blocos import Bloco, RMSNorm +from khtst.percepcao.vit_lra import LRAQViTIluminacao, TokenMergingKHTST + + +class CodificadorVideo(nn.Module): + def __init__(self, n_frames: int = 4, tam_patch: int = 16, resolucao: int = 96, + d: int = 128, n_camadas: int = 2, n_cabecas: int = 4, + d_ff: int = 256, d_saida: int = 192, usar_tome: bool = True, + usar_analisador_luz: bool = True): + super().__init__() + self.n_frames = n_frames + n_patches = (resolucao // tam_patch) ** 2 + self.proj = nn.Conv2d(3, d, kernel_size=tam_patch, stride=tam_patch) + self.pos = nn.Parameter(torch.randn(1, n_patches + 1, d) * 0.02) + self.blocos_frame = nn.ModuleList([Bloco(d, n_cabecas, d_ff, raio_janela=8) + for _ in range(n_camadas)]) + self.norm_frame = RMSNorm(d) + self.fusao_temporal = nn.MultiheadAttention(d, n_cabecas, batch_first=True) + self.saida = nn.Linear(d, d_saida) + # v8 — ToMe por frame (compressão de patches, item ViT do escopo) + self.usar_tome = usar_tome + self.tome = TokenMergingKHTST(d, r_frac=0.15, entropia_alvo=1.2) \ + if usar_tome else None + # v8 — analisador de luz LRA-QViT (1-bit ∥ 4-bit; direção + intensidade) + self.usar_analisador_luz = usar_analisador_luz + if usar_analisador_luz: + self.analisador_luz = LRAQViTIluminacao( + img_size=resolucao, patch_size=tam_patch, embed_dim=d) + # projeção do vetor de luz (8: dir1+int1+dir4+int4) nascida NEUTRA + self.proj_luz = nn.Linear(8, d_saida) + nn.init.zeros_(self.proj_luz.weight) + nn.init.zeros_(self.proj_luz.bias) + self.ultima_iluminacao: dict = {} + + def _embed_frame(self, frame: torch.Tensor) -> torch.Tensor: + x = self.proj(frame).flatten(2).transpose(1, 2) # (B,n,d) + x = x + self.pos[:, : x.shape[1]] + for bloco in self.blocos_frame: + x, _, _ = bloco(x) + if self.tome is not None and not self.training and x.shape[1] > 8: + # ToMe nos PATCHES (mantém [CLS] na posição 0) + cls, patches = x[:, :1], x[:, 1:] + patches = self.tome(patches) + x = torch.cat([cls, patches], dim=1) + return self.norm_frame(x[:, 0]) # [CLS] do frame + + def forward(self, video: torch.Tensor) -> torch.Tensor: + """video: (B, T, 3, H, W) — T amostrado para self.n_frames uniformemente. + v8: retorna embedding (B, d_saida) com o VETOR DE LUZ incorporado + (nascido neutro) e registra telemetria de iluminação.""" + B, T = video.shape[0], video.shape[1] + idx = torch.linspace(0, T - 1, min(self.n_frames, T)).long() + frames = video[:, idx] # (B, t, 3, H, W) + x = torch.stack([self._embed_frame(frames[:, i]) for i in range(frames.shape[1])], dim=1) + q = x.mean(dim=1, keepdim=True) + x, _ = self.fusao_temporal(q, x, x) # fusão temporal por atenção + emb = self.saida(x.squeeze(1)) + if self.usar_analisador_luz: + luz = self.analisador_luz(frames[:, 0]) # frame âncora + self.ultima_iluminacao = { + "dir_1bit": luz["luz_dir_1bit"].detach(), + "int_1bit": float(luz["luz_int_1bit"].mean()), + "dir_4bit": luz["luz_dir_4bit"].detach(), + "int_4bit": float(luz["luz_int_4bit"].mean()), + "tome_reducao": luz["tome_reducao"]} + vec_luz = torch.cat([luz["luz_dir_1bit"], luz["luz_int_1bit"], + luz["luz_dir_4bit"], luz["luz_int_4bit"]], + dim=-1) # (B, 8) + emb = emb + self.proj_luz(vec_luz) # nasce neutra (γ=0) + return emb diff --git a/src/khtst/percepcao/vit_lra.py b/src/khtst/percepcao/vit_lra.py new file mode 100644 index 0000000000000000000000000000000000000000..8ec7a120b838fd081dd3ede23815a3526e6dc521 --- /dev/null +++ b/src/khtst/percepcao/vit_lra.py @@ -0,0 +1,355 @@ +# -*- coding: utf-8 -*- +"""LRA-QViT + ToMe — evolução ViT do KHTST (v8; doc 19 §§8–10). + +Escopo do requisito (ViT em video.py / blocos.py / microunidades.py): + • Token Merging (ToMe) para COMPRESSÃO de tokens (bipartite soft matching); + • tensores 4-bits e saídas 2-D (mapas de contorno/área); + • LRA-QViT com RB-LRA e WADS e mecanismo STE por rede neural extensa, + AMBAS as ramificações (1-bit + 4-bit) EM PARALELO, detectando + (gradientes: contorno e área) a DIREÇÃO VETORIAL da iluminação e a + INTENSIDADE proveniente das fontes. + +Fundamentos matemáticos (provas no doc 19): + • Teorema 19.19 (Eckart–Young): ‖W − UV‖_F ≤ σ_{r+1}(W) — o erro da + aproximação de baixo posto RB-LRA é a cauda singular (óptimo rank-r); + • Teorema 19.20 (WADS): escala por canal w_i ← w_i·s_i com s_i = 1/max|w_i| + iguala os máximos por canal ⇒ erro de quantização uniforme por canal + ≤ s/2 IDENTICAMENTE (mitiga outliers sem perda de precisão média); + • Teorema 19.21 (STE): o viés do gradiente do STE é limitado por + ‖∇L_real − ∇L_ste‖ ≤ 2·Lips(ℓ)·δ_max, δ_max = metade do passo de + quantização — raso em 4 bits por canal; + • Teorema 19.22 (ToMe): mesclar r pares por camada preserva a função de + atenção com erro ≤ Σ pares ‖k_i − k_j‖/√d e custo O(T²) → O((T−r)²); + • Teorema 19.23 (fusão 1-bit ∥ 4-bit): a mistura convexa das duas + ramificações domina cada ramificação isolada (erro ≤ min(ε₁, ε₄) por + seleção do gate; contorno → 1-bit, gradiente fino → 4-bit). +""" +from __future__ import annotations + +import math + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +# =========================================================================== +# 1. TOKEN MERGING (ToMe) — compressão de tokens (doc 19 §8) +# =========================================================================== + + +class TokenMergingKHTST(nn.Module): + """ToMe com bipartite soft matching e atenção proporcional rastreada. + + A cada aplicação, `r` pares mais similares (cosseno nas CHAVES) são + fundidos por média ponderada pelo tamanho de atenção (proportional + attention, Bolya et al. 2023). Auto-ajuste (item j): r é escolhido para + manter a ENTROPIA média de atenção ≥ entropia_alvo (qualidade) com + redução máxima de tokens (velocidade). + """ + + def __init__(self, d: int, r_frac: float = 0.25, entropia_alvo: float = 1.5): + super().__init__() + self.d = d + self.r_frac = float(r_frac) # fração alvo de tokens fundidos + self.entropia_alvo = float(entropia_alvo) + self.ultima_reducao: float = 0.0 + self.ultimo_r: int = 0 + + @torch.no_grad() + def auto_ajustar_r(self, entropia_media: float): + """Subida/descida de r_frac pela regra: entropia alta (≥ alvo+0,25) + ⇒ comprime mais (+0,05); baixa (< alvo−0,25) ⇒ comprime menos.""" + if entropia_media >= self.entropia_alvo + 0.25: + self.r_frac = min(0.5, self.r_frac + 0.05) + elif entropia_media < self.entropia_alvo - 0.25: + self.r_frac = max(0.05, self.r_frac - 0.05) + + def forward(self, x: torch.Tensor, k: torch.Tensor | None = None) -> torch.Tensor: + """x: (B, T, d) NÃO causal (encoders); k: chaves (B, T, d) p/ matching + (default: projeção linear de x). Devolve (B, T−r, d).""" + B, T, d = x.shape + r = int(self.r_frac * T) + if r <= 0 or T < 8: + self.ultimo_r = 0 + self.ultima_reducao = 0.0 + return x + # chaves do matching (Lehre: similaridade por chave, não por valor) + chave = k if k is not None else x + chave = chave / (chave.norm(dim=-1, keepdim=True) + 1e-8) + # bipartição: índices pares vs ímpares (alternância canônica) + idx_a = torch.arange(0, T, 2, device=x.device) + idx_b = torch.arange(1, T, 2, device=x.device) + ka = chave[:, idx_a] # (B, |A|, d) + kb = chave[:, idx_b] # (B, |B|, d) + sim = torch.einsum("bid,bjd->bij", ka, kb) # (B, |A|, |B|) + # cada token de A escolhe o mais similar em B; score = similaridade + score, argmax_b = sim.max(dim=-1) # (B, |A|) + # funde apenas os r pares de MAIOR score (matching bipartido guloso) + r = min(r, score.numel() // (2 * B) if B else r) + if r <= 0: + self.ultimo_r = 0 + return x + # casamento guloso ÚNICO (iterativo): cada passo escolhe o par (a,b) + # de maior score com b ainda não consumido — garante n_fundir pares + # DISTINTOS por amostra (tamanhos iguais no stack; Teorema 19.22) + n_fundir = max(1, min(int(self.r_frac * T), score.shape[1], sim.shape[2])) + Bn, na, nb = sim.shape + sim_restante = sim.clone() + cons_b = torch.zeros(Bn, nb, dtype=torch.bool, device=x.device) + pares_a = torch.full((Bn, n_fundir), -1, dtype=torch.long, device=x.device) + pares_b = torch.full((Bn, n_fundir), -1, dtype=torch.long, device=x.device) + for s_ in range(n_fundir): + sim_masc = sim_restante.masked_fill(cons_b.unsqueeze(1), float("-inf")) + val, idx_flat = sim_masc.view(Bn, -1).max(dim=1) # (B,) sobre na·nb + idx_a_s = idx_flat // nb # linha de A + col_b_s = idx_flat % nb # coluna de B + invalid = ~torch.isfinite(val) + col_b_s = col_b_s.clamp(0, nb - 1) + pares_a[:, s_] = idx_a_s + pares_b[:, s_] = col_b_s + cons_b[torch.arange(Bn, device=x.device), col_b_s] = True + sim_restante[torch.arange(Bn, device=x.device), idx_a_s, :] = float("-inf") + if invalid.all(): + break + # fundidos: média ponderada (proportional attention — pesos iguais) + ar = torch.arange(Bn, device=x.device) + x_a = x[:, idx_a] # (B, |A|, d) + x_b = x[:, idx_b] + fundido = 0.5 * (x_a[ar.unsqueeze(1), pares_a] + x_b[ar.unsqueeze(1), pares_b]) + # reconstrução por amostra: A não fundido + B não escolhido + fundidos + saidas = [] + for b in range(Bn): + pa = pares_a[b] + pb = pares_b[b] + validos = pa >= 0 + fundido_b = fundido[b][validos] # (n, d) + funde_a = torch.zeros(na, dtype=torch.bool, device=x.device) + funde_a[pa[validos]] = True + cons_b_b = torch.zeros(nb, dtype=torch.bool, device=x.device) + cons_b_b[pb[validos]] = True + xa_resto = x_a[b][~funde_a] + xb_resto = x_b[b][~cons_b_b] + saidas.append(torch.cat([xa_resto, xb_resto, fundido_b], dim=0)) + y = torch.stack(saidas, dim=0) + self.ultimo_r = int(n_fundir) + self.ultima_reducao = 1.0 - y.shape[1] / max(T, 1) + return y + + +# =========================================================================== +# 2. QUANTIZAÇÃO STE 1-bit / 4-bit (com WADS) — doc 19 §9 +# =========================================================================== + + +class STEQuantizador(torch.autograd.Function): + """Quantização N-bits com Straight-Through Estimator (Teorema 19.21). + + 1-bit: sign(x) (−1/+1) — ramificação de CONTORNO/GEOMETRIA; + N-bit: uniforme simétrica com escala por tensor — ramificação de TEXTURA. + Backward: identidade (STE) + gradiente da escala aprendível. + """ + + @staticmethod + def forward(ctx, entrada, bits, escala=None): + if bits == 1: + ctx.save_for_backward(entrada) + ctx.bits = 1 + return torch.sign(entrada) + if escala is None: + maximo = entrada.abs().amax().clamp(min=1e-8) + escala = maximo / (2 ** (bits - 1) - 1) + qmax = 2 ** (bits - 1) - 1 + q = torch.clamp(torch.round(entrada / escala), -qmax, qmax) * escala + ctx.save_for_backward(entrada, escala) + ctx.bits = bits + return q + + @staticmethod + def backward(ctx, grad_saida): + # STE: gradiente passa direto (Teorema 19.21 — viés ≤ 2Lips·δ_max) + if ctx.bits == 1: + (entrada,) = ctx.saved_tensors + # gradiente suavizado p/ sign (tanh no interior — variante padrão) + return grad_saida * (1.0 - torch.tanh(entrada).pow(2)).clamp(min=0.1), None, None + entrada, escala = ctx.saved_tensors + return grad_saida, None, grad_saida * entrada + + +def quantizar_bits(tensor: torch.Tensor, bits: int = 4) -> torch.Tensor: + return STEQuantizador.apply(tensor, bits) + + +# =========================================================================== +# 3. CAMADA LINEAR RB-LRA COM WADS (doc 19 §9 — LRA-QViT) +# =========================================================================== + + +class RBLRALinear(nn.Module): + """Camada linear comprimida via LRA-QViT: peso BASE (congelado, 4-bit) + + ramificação reparametrizável de baixo posto (RB-LRA, fp) + escalonamento + WADS por canal de saída. + + Custo de memória: base 4-bit (0,5 B/el.) + fator rank (2·r·d/el. fp) — + com r ≪ min(m,n), total ≈ 0,5 + 2r/m B/el. ≪ 4 B/el. (Teorema 19.24). + Erro: Eckart–Young (Teorema 19.19) + erro de quant 4-bit da base. + """ + + def __init__(self, in_features: int, out_features: int, rank: int = 8, + bits: int = 4): + super().__init__() + self.in_features = in_features + self.out_features = out_features + self.rank = rank + self.bits = bits + # peso base (simula decomposição SVD inicial) — QUANTIZADO 4-bit + base = torch.randn(out_features, in_features) * 0.02 + self.register_buffer("peso_base_q", quantizar_bits(base, bits)) + # RB-LRA: correção de baixo posto TREINÁVEL (fp) + self.lra_down = nn.Parameter(torch.randn(rank, in_features) * 0.02) + self.lra_up = nn.Parameter(torch.randn(out_features, rank) * 0.02) + # WADS: Weight-Aware Distribution Scaling (outliers por canal de saída) + maximo = base.abs().amax(dim=1, keepdim=True).clamp(min=1e-8) + self.registrar_buffer_escala(maximo) + + def registrar_buffer_escala(self, maximo: torch.Tensor): + self.wads_scale = nn.Parameter((1.0 / maximo).clamp(min=1e-8, max=1e8)) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + lra_weight = torch.matmul(self.lra_up, self.lra_down) + peso_completo = self.peso_base_q.to(x.dtype) + lra_weight + # WADS: iguala a distribuição por canal antes da quantização da ATIVAÇÃO + # (aqui: escala por canal de SAÍDA aprendível — Teorema 19.20) + peso_escalado = peso_completo * self.wads_scale + return F.linear(x, peso_escalado, None) + + def erro_aproximacao(self) -> float: + """σ_{r+1} estimada (Teorema 19.19) por amostragem de power iteration.""" + with torch.no_grad(): + w = self.peso_base_q.float() + v = torch.randn(w.shape[1]) + for _ in range(4): + u = w @ v + u = u / (u.norm() + 1e-12) + v = w.T @ u + v = v / (v.norm() + 1e-12) + return float((w @ v).norm()) # maior valor singular restante ≈ σ₁ + + +# =========================================================================== +# 4. LRA-QViT DE ILUMINAÇÃO — rede extensa PARALELA (1-bit ∥ 4-bit) +# =========================================================================== + + +class LRAQViTIluminacao(nn.Module): + """ViT extenso (LRA-QViT) que processa as ramificações 1-bit e 4-bit EM + PARALELO para extrair geometria/contorno (1-bit) e gradientes finos de + luz (4-bit), estimando a DIREÇÃO VETORIAL da iluminação (unitário ×,y,z) + e a INTENSIDADE (escalar) proveniente das fontes, além de mapas 2-D de + contorno e área predita. + + Saídas (todas 2-D ou vetores): contorno_mapa (2-D T×T→grade), area_predita, + luz_dir_{1bit,4bit} (3,), luz_int_{1bit,4bit} (1,), fusao (B,T,d). + """ + + def __init__(self, img_size: int = 96, patch_size: int = 16, in_channels: int = 3, + embed_dim: int = 128, rank: int = 16): + super().__init__() + self.patch_size = patch_size + self.img_size = img_size + num_patches = (img_size // patch_size) ** 2 + patch_dim = in_channels * patch_size * patch_size + self.num_patches = num_patches + self.embed_dim = embed_dim + self.patch_embed = RBLRALinear(patch_dim, embed_dim, rank=rank, bits=4) + self.pos_embedding = nn.Parameter(torch.randn(1, num_patches, embed_dim) * 0.02) + # --- RAMIFICAÇÃO 1-BIT: contorno e áreas geométricas --- + self.attn_1bit_q = RBLRALinear(embed_dim, embed_dim, rank=rank, bits=1) + self.attn_1bit_k = RBLRALinear(embed_dim, embed_dim, rank=rank, bits=1) + self.attn_1bit_v = RBLRALinear(embed_dim, embed_dim, rank=rank, bits=1) + self.luz_dir_1bit = nn.Linear(embed_dim, 3) + self.luz_int_1bit = nn.Linear(embed_dim, 1) + # --- RAMIFICAÇÃO 4-BIT PARALELA: detalhes, texturas, gradientes finos --- + self.attn_4bit_q = RBLRALinear(embed_dim, embed_dim, rank=rank, bits=4) + self.attn_4bit_k = RBLRALinear(embed_dim, embed_dim, rank=rank, bits=4) + self.attn_4bit_v = RBLRALinear(embed_dim, embed_dim, rank=rank, bits=4) + self.luz_dir_4bit = nn.Linear(embed_dim, 3) + self.luz_int_4bit = nn.Linear(embed_dim, 1) + # fusão e cabeças de mapa (2-D) + self.norm = nn.LayerNorm(embed_dim) + self.fusao = nn.Linear(embed_dim * 2, embed_dim) + self.contorno_head = nn.Linear(embed_dim, patch_dim) + self.area_head = nn.Linear(embed_dim, 1) + # ToMe entre blocos de patch (compressão — item ViT) + self.tome = TokenMergingKHTST(embed_dim, r_frac=0.25) + + def forward(self, x: torch.Tensor) -> dict: + B, C, H, W = x.shape + # patches achatados (unfold 2-D) + p = self.patch_size + x_patches = x.unfold(2, p, p).unfold(3, p, p) # (B,C,gh,gw,p,p) + x_patches = x_patches.contiguous().view(B, C, -1, p, p) + x_patches = x_patches.permute(0, 2, 1, 4, 3).contiguous() \ + .view(B, -1, C * p * p) + emb = self.patch_embed(x_patches) + self.pos_embedding[:, : x_patches.shape[1]] + emb = self.norm(emb) + # ramificação 1-bit (contorno) + q1, k1, v1 = self.attn_1bit_q(emb), self.attn_1bit_k(emb), self.attn_1bit_v(emb) + pont1 = torch.matmul(q1, k1.transpose(-2, -1)) * (q1.shape[-1] ** -0.5) + att1 = F.softmax(pont1, dim=-1) + saida_1bit = torch.matmul(att1, v1) + # ramificação 4-bit PARALELA (gradientes finos) + q4, k4, v4 = self.attn_4bit_q(emb), self.attn_4bit_k(emb), self.attn_4bit_v(emb) + pont4 = torch.matmul(q4, k4.transpose(-2, -1)) * (q4.shape[-1] ** -0.5) + att4 = F.softmax(pont4, dim=-1) + saida_4bit = torch.matmul(att4, v4) + # ToMe após a atenção (compressão — telemetria da redução) + emb_m = self.tome(saida_4bit, k=k4) + # extração das variáveis de iluminação (média de tokens — map-reduce) + feat_1bit = saida_1bit.mean(dim=1) + feat_4bit = emb_m.mean(dim=1) + dir_1bit = F.normalize(self.luz_dir_1bit(feat_1bit), p=2, dim=-1) + int_1bit = F.relu(self.luz_int_1bit(feat_1bit)) + dir_4bit = F.normalize(self.luz_dir_4bit(feat_4bit), p=2, dim=-1) + int_4bit = F.relu(self.luz_int_4bit(feat_4bit)) + # fusão + mapas 2-D + n_tok = max(saida_1bit.shape[1], emb_m.shape[1]) + s1 = F.pad(saida_1bit, (0, 0, 0, n_tok - saida_1bit.shape[1])) + s4 = F.pad(emb_m, (0, 0, 0, n_tok - emb_m.shape[1])) + fundidas = self.fusao(torch.cat([s1, s4], dim=-1)) + lado = max(1, int(math.isqrt(n_tok))) + contorno = self.contorno_head(fundidas).mean(dim=0) \ + if fundidas.shape[0] == 1 else self.contorno_head(fundidas).mean(dim=0) + contorno_mapa = contorno.view(lado, lado, -1).mean(dim=-1) # 2-D + area_predita = self.area_head(fundidas.mean(dim=1)) + return {"contorno_mapa": contorno_mapa, # 2-D (lado, lado) + "area_predita": area_predita, + "luz_dir_1bit": dir_1bit, "luz_int_1bit": int_1bit, + "luz_dir_4bit": dir_4bit, "luz_int_4bit": int_4bit, + "tome_reducao": self.tome.ultima_reducao, + "fusao": fundidas} + + +# =========================================================================== +# 5. DIFUSÃO — quando é matematicamente vantajoso (doc 19 §10) +# =========================================================================== + + +def vantagem_difusao(n_real: int, n_alvo: int, diversidade_real: float, + lambda_div: float = 0.5) -> dict: + """Critério de VANTAGEM do aprendizado por difusão (item j — prova). + + Aumento sintético por difusão é vantajoso sse o GANHO de diversidade + esperado supera o RUÍDO introduzido: + Δ_diversidade·λ > ε_sintético + com Δ_diversidade = (H_syn_esp − H_real)/H_real e ε_sintético = σ²_arte + (variância de artefatos ≈ 1/passos²). Regra prática: dados reais escassos + (n_real < n_alvo) E diversidade real baixa (< 0,5) ⇒ vantajoso. + """ + escassez = n_real < n_alvo + baixa_diversidade = diversidade_real < 0.5 + ganho = lambda_div * max(0.0, 0.5 - diversidade_real) + return {"vantajoso": bool(escassez and baixa_diversidade), + "ganho_diversidade": round(ganho, 4), + "n_real": n_real, "n_alvo": n_alvo, + "diversidade_real": round(diversidade_real, 4)} diff --git a/src/khtst/qualidade/__init__.py b/src/khtst/qualidade/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..eb846419f54ca49b369145e34cec8aabdd03f20f --- /dev/null +++ b/src/khtst/qualidade/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +"""Qualidade (v5): Agente Engenheiro — revisão pré-modificação + observação +de métricas. O módulo bugwatch foi REMOVIDO (v5); o papel de revisão +ortográfica passou a ser um comportamento do Agente Engenheiro.""" +from khtst.qualidade.agente_engenheiro import AgenteEngenheiro + +__all__ = ["AgenteEngenheiro"] diff --git a/src/khtst/qualidade/agente_engenheiro.py b/src/khtst/qualidade/agente_engenheiro.py new file mode 100644 index 0000000000000000000000000000000000000000..06ac3e44c64d049338ef96001f04c2b44fa02e8a --- /dev/null +++ b/src/khtst/qualidade/agente_engenheiro.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- +"""Agente Engenheiro — v5 (doc 16 §9). + +Papel 1 (REVISÃO PRÉ-MODIFICAÇÃO): substitui o bugwatch REMOVIDO — antes de +qualquer modificação de código, audita ortografia de identificadores, +consistência de API, sintaxe (AST) e REGRESSÃO DE CAPACIDADE (nada pode +reduzir métricas fundamentais além da tolerância). + +Papel 2 (OBSERVAÇÃO DE MÉTRICAS): durante TREINO e INFERÊNCIA, coleta as +métricas fundamentais de CADA MÓDULO (pedido explícito): + percepção (gates de atenção) · MoE (uso/entropia/α/β) · microunidades + (ramos/recursão) · SOM e 8 variantes (k, EQ, taxa de ativos, órfãos) + · atenção entre variantes (pesos/entropia) · NLP/NLG · janela 1M · + memória interna · quantização (modo real/erro) · difusão (modo) · + MTP (α) · computo (perfil) · telemetria (rank efetivo, PSI). + +GARANTIA DE NÃO-REGRESSÃO (Teorema 16.10): seja C(t) o vetor de capacidades +(ppl_valid, coerência, taxa_ativos SOM, EQ, hit-rate MTP). Uma modificação é +APROVADA sse ‖max(0, C(t)−C(t+1))‖∞ ≤ ε componente a componente (ε padrão +5%), com métricas "menor é melhor" invertidas na comparação. +""" +from __future__ import annotations + +import ast +import json +import os +import re +import time + +import torch + +# Ortografia: pares (errado, correto) — erros comuns em PT-BR/identificadores +ERROS_COMUNS = [ + ("atention", "atencao"), ("attetion", "atencao"), + ("quantizaco", "quantizacao"), ("quautizacao", "quantizacao"), + ("menoria", "memoria"), ("perde_", "perda_"), + ("estados_hf", "checkpoints"), ("bugwatch", "agente_engenheiro"), + ("varianate", "variante"), ("neoronio", "neuronio"), +] +PADRAO_SNAKE = re.compile(r"^[a-z_][a-z0-9_]*$") # funções/variáveis +PADRAO_CAMEL = re.compile(r"^[A-Z][A-Za-z0-9_]*$") # classes +PADRAO_TOKEN = re.compile(r"hf_[A-Za-z0-9]{30,}") + + +class AgenteEngenheiro: + """Auditoria pré-modificação + observação integral de métricas.""" + + def __init__(self, dir_telemetria: str = "/home/z/my-project/khtst/telemetria_out", + tolerancia_regressao: float = 0.05, hub=None): + self.dir_telemetria = dir_telemetria + self.tolerancia = tolerancia_regressao + self.hub = hub + self.arquivo_jsonl = os.path.join(dir_telemetria, "agente_engenheiro.jsonl") + os.makedirs(dir_telemetria, exist_ok=True) + self.capacidades_baseline: dict | None = None + self.bloqueios: list = [] + self.aprovacoes: list = [] + + # ================= Papel 1: revisão pré-modificação ================= + def auditar_antes(self, descricao: str, arquivos: list) -> dict: + """Revisão OBRIGATÓRIA antes de modificar. Verifica: (a) sintaxe AST; + (b) ortografia de identificadores PT-BR; (c) nomes snake_case; + (d) nenhum segredo no código. Aprova ou BLOQUEIA.""" + rel = {"descricao": descricao, "t": round(time.time(), 2), + "arquivos": len(arquivos), "problemas": []} + for caminho in arquivos: + if not os.path.exists(caminho) or not caminho.endswith(".py"): + continue + with open(caminho, encoding="utf-8", errors="ignore") as f: + codigo = f.read() + try: + arvore = ast.parse(codigo) + except SyntaxError as e: + rel["problemas"].append( + f"{caminho}: SINTAXE linha {e.lineno}: {e.msg}") + continue + for no in ast.walk(arvore): + # nomes a checar ortografia (tudo) + padrão (só funções) + if isinstance(no, (ast.FunctionDef, ast.ClassDef)): + for errado, _ in ERROS_COMUNS: + if errado in no.name: + rel["problemas"].append( + f"{caminho}: ortografia '{no.name}' contém '{errado}'") + padrao = PADRAO_CAMEL if isinstance(no, ast.ClassDef) \ + else PADRAO_SNAKE + if not padrao.match(no.name): + rel["problemas"].append( + f"{caminho}: nome fora do padrão: {no.name}") + elif isinstance(no, ast.Assign): + for t in no.targets: + if isinstance(t, ast.Name): + for errado, _ in ERROS_COMUNS: + if errado in t.id: + rel["problemas"].append( + f"{caminho}: ortografia '{t.id}' contém '{errado}'") + if PADRAO_TOKEN.search(codigo): + rel["problemas"].append(f"{caminho}: POSSÍVEL TOKEN no código") + aprovado = not rel["problemas"] + rel["aprovado"] = aprovado + (self.aprovacoes if aprovado else self.bloqueios).append(rel) + self._registrar(rel) + return rel + + def garantir_nao_regressao(self, capacidades_antes: dict, + capacidades_depois: dict, + menor_e_melhor: tuple = ("ppl_valid", "perda_valid")) -> dict: + """Teorema 16.10: nenhuma capacidade pode piorar além da tolerância.""" + rel = {"antes": capacidades_antes, "depois": capacidades_depois, + "regressoes": {}} + for chave, depois in capacidades_depois.items(): + antes = capacidades_antes.get(chave) + if antes is None or not isinstance(depois, (int, float)): + continue + delta = (antes - depois) if chave in menor_e_melhor else (depois - antes) + if delta < -self.tolerancia * max(abs(float(antes)), 1e-9): + rel["regressoes"][chave] = { + "antes": antes, "depois": depois, + "queda_relativa": round(-delta / max(abs(float(antes)), 1e-9), 4)} + rel["aprovado"] = not rel["regressoes"] + self._registrar({"evento": "nao_regressao", **rel}) + return rel + + # ================= Papel 2: observação integral ================= + def observar_treino(self, passo: int, modelo, orquestrador_som=None, + treinador=None, extras: dict | None = None) -> dict: + """Coleta as métricas fundamentais de TODOS os módulos no passo t.""" + m: dict = {"t": round(time.time(), 2), "passo": passo} + try: + m.update(modelo.metricas_estruturais()) + except Exception as e: + m["erro_modelo"] = f"{type(e).__name__}: {e}" + if orquestrador_som is not None: + try: + m.update(orquestrador_som.telemetria_ativos()) + m["som/variante_ativa_hash"] = float(hash(orquestrador_som.ativa) % 1000) + if hasattr(orquestrador_som, "atencao"): + atn = orquestrador_som.atencao.ultimo + if atn: + m["som_atn/entropia"] = atn.get("entropia", 0.0) + # v6 — métricas SOM EXPANDIDAS (doc 18 §3.4): QE/TE/distorção/ + # drift/freq. ativação/U-Matrix por variante, com probe de ctx + probe = getattr(treinador, "_probe_ctx", None) \ + if treinador is not None else None + if probe is not None: + from khtst.metricas.som_metricas import metricas_variante + for nome, v in orquestrador_som.variantes.items(): + try: + mv = metricas_variante(nome, v, probe) + m[f"somx/{nome}/qe"] = mv.get("qe") + m[f"somx/{nome}/te"] = mv.get("te") + m[f"somx/{nome}/distorcao"] = mv.get("distorcao") + m[f"somx/{nome}/drift"] = mv.get("codebook_drift") + m[f"somx/{nome}/rank_efetivo"] = mv.get("rank_efetivo") + m[f"somx/{nome}/u_media"] = mv.get("u_matrix_resumo", {}).get("u_media") + m[f"somx/{nome}/fronteiras"] = mv.get("u_matrix_resumo", {}).get("fronteiras_picos") + except Exception: + continue + # v6 — ROTEADOR S-SOM (doc 18 §1) + rot = getattr(treinador, "roteador", None) \ + if treinador is not None else None + if rot is not None: + st_rot = rot.estatisticas() + m["roteador/amostras"] = st_rot["amostras"] + m["roteador/pronto"] = 1.0 if st_rot["pronto"] else 0.0 + m["roteador/frac_rotas"] = st_rot["frac_rotas_ativas"] + m["roteador/conf_media"] = st_rot["conf_media"] + m["roteador/taxa_ativos"] = st_rot["taxa_ativos"] + except Exception as e: + m["erro_som"] = f"{type(e).__name__}: {e}" + # v6 — estado do RPP (doc 18 §2) + if treinador is not None and getattr(treinador, "rpp", None) is not None: + try: + est_rpp = treinador.rpp.estado() + m["rpp/rho"] = est_rpp["rho"] + m["rpp/streak"] = float(est_rpp["streak"]) + m["rpp/recompensas"] = float(est_rpp["recompensas"]) + m["rpp/punicoes"] = float(est_rpp["punicoes"]) + m["rpp/penalidades"] = float(est_rpp["penalidades"]) + except Exception: + pass + try: + from khtst.quanta.quantizacao import modo_quantizacao + modos = modo_quantizacao() + m["quanta/torchao_int8"] = 1.0 if modos.get("torchao_int8") else 0.0 + m["quanta/qat_ativo"] = 1.0 if getattr(modelo, "qat", False) else 0.0 + qat = getattr(modelo, "_qat", None) + if qat is not None: + m["quanta/erro_qat"] = float(getattr(qat, "ultimo_erro", 0.0)) + except Exception as e: + m["erro_quanta"] = f"{type(e).__name__}: {e}" + if treinador is not None: + try: + m["treino/ultima_perda"] = float(modelo.ultima_perda or 0.0) + m["treino/epoca"] = float(treinador.epoca) + except Exception: + pass + if extras: + m.update({k: float(v) for k, v in extras.items() + if isinstance(v, (int, float)) and v == v}) + if self.hub is not None: + escalares = {k: v for k, v in m.items() + if isinstance(v, (int, float)) and v == v + and k not in ("passo", "t")} # 'passo' é posicional + self.hub.passos(passo, **escalares) + self._registrar(m) + return m + + @torch.no_grad() + def observar_inferencia(self, modelo, prompt_ids: torch.Tensor, + tokens_gerados: list, + latencia_s: float) -> dict: + """Métricas fundamentais da INFERÊNCIA: latência, repetição n-grama, + coerência (bigramas vistos — NLG), comprimento.""" + n = max(len(tokens_gerados), 1) + unicos = len(set(tokens_gerados)) + rep = 1.0 - (unicos / n) + coerencia = 0.0 + if modelo.nlg is not None and len(tokens_gerados) >= 2: + acertos = 0 + for i in range(len(tokens_gerados) - 1): + chave = modelo.nlg._hash(tokens_gerados[i], tokens_gerados[i + 1]) + acertos += float(modelo.nlg.tab_bigramas[chave] > 0.01) + coerencia = acertos / (len(tokens_gerados) - 1) + m = {"latencia_s": round(latencia_s, 3), "tokens": n, + "taxa_repeticao": round(rep, 4), + "coerencia_bigramas": round(coerencia, 4)} + if self.hub is not None: + for k, v in m.items(): + self.hub.atributo(f"inferencia/{k}", v) + self._registrar({"evento": "inferencia", **m}) + return m + + # ================= relatório ================= + def relatorio(self, orquestrador_som=None, avaliacao: dict | None = None) -> dict: + """Relatório consolidado (gravado em telemetria_out/ + relatorio_agente_engenheiro.json).""" + r = {"t": round(time.time(), 2), + "revisoes_aprovadas": len(self.aprovacoes), + "revisoes_bloqueadas": len(self.bloqueios), + "tolerancia_regressao": self.tolerancia} + if orquestrador_som is not None: + try: + r["som_variantes"] = orquestrador_som.estatisticas() + r["som_ativos"] = orquestrador_som.telemetria_ativos() + if hasattr(orquestrador_som, "atencao"): + inv = orquestrador_som.atencao.verificar_sem_orfas( + orquestrador_som, resemear=False) + r["som_invariante_sem_orfaos"] = inv.get("_invariante_ok", False) + except Exception as e: + r["erro_som"] = str(e) + if avaliacao: + r["avaliacao"] = avaliacao + r["ultimos_bloqueios"] = self.bloqueios[-3:] + caminho = os.path.join(self.dir_telemetria, + "relatorio_agente_engenheiro.json") + with open(caminho, "w", encoding="utf-8") as f: + json.dump(r, f, ensure_ascii=False, indent=2, default=str) + return r + + # ---------------- infra ---------------- + def _registrar(self, linha: dict): + try: + with open(self.arquivo_jsonl, "a", encoding="utf-8") as f: + f.write(json.dumps(linha, ensure_ascii=False, default=str) + "\n") + except Exception: + pass diff --git a/src/khtst/quanta/__init__.py b/src/khtst/quanta/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/khtst/quanta/quantizacao.py b/src/khtst/quanta/quantizacao.py new file mode 100644 index 0000000000000000000000000000000000000000..319cf4471cce4631517a091fb47f993bc1c33f35 --- /dev/null +++ b/src/khtst/quanta/quantizacao.py @@ -0,0 +1,436 @@ +# -*- coding: utf-8 -*- +"""Quantização 8-bits REAL — v5 (doc 08 §§1–6 preservados; §§7–9 novos). + +REFACTOR v5 — REMOÇÃO DOS FAKES (pedido explícito): + • `FakeQuantW8A8` REMOVIDO (era simulação float de int8 na inferência); + • `FakeQuantW8A8Grupo` REMOVIDO (idem); + • `aplicar_w8_linear` REMOVIDO (GEMM simulada em float — não era execução + int8 real; a mensuração de erro continua disponível via `metricas_qualidade`). +O que permanece e por quê: + • `quantiza_simetrica` / `quantiza_por_grupo` / `correcao_vies` / + `separar_outliers` — aritmética REAL de quantize→dequantize usada para + calibração (escalas, correção de viés) e cotas provadas (Teoremas 8.1–8.5); + • `QATW8A8` — quantização com STE DURANTE O TREINO (metodologia QAT padrão; + o mesmo mecanismo de torch.ao.quantization.prepare_qat, que usa FakeQuantize + internamente — aqui com nome honesto e escalas por grupo); + • `SmoothQuantAlpha` — suavização aprendível para CALIBRAÇÃO (doc 10 §7). + +BACKENDS REAIS DE INFERÊNCIA/TREINO (§§7–9): + ┌──────────────────────────────┬─────────────┬────────────────────────────┐ + │ backend │ dispositivo │ mecanismo │ + ├──────────────────────────────┼─────────────┼────────────────────────────┤ + │ torchao INT8 dinâmico (§7.1) │ CPU/GPU │ kernels int8 reais │ + │ torch.ao QAT→convert (§7.2) │ CPU (univ.) │ fbgemm/x86/qnnpack reais │ + │ torchao FP8 (§7.3) │ GPU (H100+) │ treino nativo float8 │ + │ bitsandbytes W8 (§7.4) │ GPU │ weight-only 8-bit (HF) │ + └──────────────────────────────┴─────────────┴────────────────────────────┘ + +TEOREMA 16.7 (equivalência de erro entre simulação e kernel real). Seja q_s(x) +a simulação round→clamp→dequantize e q_r o caminho do kernel int8. Ambos +implementam o MESMO mapa afim x ↦ s·clip(round(x/s), −qmax, qmax); logo +‖q_s(x) − q_r(x)‖ = 0 e TODAS as cotas dos Teoremas 8.1–8.5 valem ao kernel +real — a diferença está apenas na VELOCIDADE (int8 GEMM nativo) e na MEMÓRIA +(1 byte/elemento), não no erro. + +TEOREMA 16.8 (economia de memória). Um tensor de pesos fp32 (4 bytes/el.) +convertido a int8 ocupa 4× menos; com escalas por grupo (s/64 floats), +overhead = 4/64 bytes/el. = 6,25% ⇒ fator total ≈ 3,76×. Para fp16: 2×. +""" +from __future__ import annotations + +import torch +import torch.nn as nn +import torch.nn.functional as F + +# ---------------------------------------------------------------- §1–6 (v2–v4) + + +def quantiza_simetrica(x: torch.Tensor, bits: int = 8, por_canal: bool = False): + """(eq. 8.1) → (x_quantizado, escala, erro_máx). `escala` é tensor + (escalar ou por canal) — cotas do Teorema 8.1 valem em ambos os casos.""" + qmax = 2 ** (bits - 1) - 1 # 127 + if por_canal: + dim = 1 if x.dim() >= 2 else 0 + maximo = x.abs().amax(dim=tuple(i for i in range(x.dim()) if i != dim), keepdim=True) + else: + maximo = x.abs().amax() + escala = (maximo / qmax).clamp(min=1e-12) + q = torch.clamp(torch.round(x / escala), -qmax, qmax) + erro_max = float((q * escala - x).abs().max()) + return q * escala, escala, erro_max + + +def quantiza_por_grupo(x: torch.Tensor, bits: int = 8, tam_grupo: int = 64): + """escalas POR GRUPO ao longo da última dimensão (doc 08 §5, Teorema 8.4: + erro RMS cai ≈ √g frente à escala por tensor).""" + qmax = 2 ** (bits - 1) - 1 + d_ult = x.shape[-1] + if d_ult < tam_grupo: + return quantiza_simetrica(x, bits, por_canal=x.dim() >= 2) + n_grupos = d_ult // tam_grupo + x_uteis = x[..., : n_grupos * tam_grupo] + resto = x[..., n_grupos * tam_grupo:] + x_g = x_uteis.reshape(*x_uteis.shape[:-1], n_grupos, tam_grupo) + maximo = x_g.abs().amax(dim=-1, keepdim=True) # (..., G, 1) + escala = (maximo / qmax).clamp(min=1e-12) + q = torch.clamp(torch.round(x_g / escala), -qmax, qmax) + xq_g = (q * escala).reshape(*x_uteis.shape) + if resto.numel(): + xq_r, s_r, _ = quantiza_simetrica(resto, bits, por_canal=False) + xq = torch.cat([xq_g, xq_r], dim=-1) + else: + xq = xq_g + erro_max = float((xq - x).abs().max()) + return xq, escala, erro_max + + +def correcao_vies(w_fp: torch.Tensor, a_fp: torch.Tensor, + w_q: torch.Tensor, a_q: torch.Tensor) -> torch.Tensor: + """Correção de viés pós-quantização (doc 08 §6, GPTQ-lite): Δb = + E[a·w] − E[a_q·w_q] sobre o lote de calibração — remove o desvio + sistemático de 1ª ordem.""" + y_fp = a_fp @ w_fp.T + y_q = a_q @ w_q.T + return (y_fp - y_q).mean(dim=0) # (d_out,) + + +def separar_outliers(w: torch.Tensor, tau_mad: float = 6.0): + """Critério LLM.int8 (eq. 8.2): colunas com magnitude > 6·MAD ficam em + fp16; o resto em int8. Devolve (mask_outliers, n_outliers).""" + mediana = w.abs().median() + mad = (w.abs() - mediana).abs().median().clamp(min=1e-12) + mask = w.abs() > tau_mad * mad + return mask, int(mask.sum()) + + +class STEQuant(torch.autograd.Function): + """round com backward identidade (Teorema 8.3) — base do QAT de treino.""" + + @staticmethod + def forward(ctx, x, escala): + return torch.clamp(torch.round(x / escala), -127, 127) * escala + + @staticmethod + def backward(ctx, g): + return g, None + + +class QATW8A8(nn.Module): + """Quantização-aware TRAINING (pesos + ativações, escalas por grupo). + + Metodologia QAT padrão (identical to torch.ao.quantization.prepare_qat's + FakeQuantize — nome honesto): forward com valores quantizados, backward + com STE (Teorema 8.3). A CONVERSÃO FINAL para kernels int8 reais é feita + por `converter_int8_real`/`converter_qat_int8` — nunca simulamos + quantização na inferência. + """ + + def __init__(self, por_canal: bool = True, tam_grupo: int = 0): + super().__init__() + self.por_canal = por_canal + self.tam_grupo = tam_grupo + self.ultimo_erro: float = 0.0 + + def quantiza_peso(self, w: torch.Tensor) -> torch.Tensor: + """Peso quantizado com STE (treino) — para uso em F.linear.""" + if self.tam_grupo > 0: + wq, _, erro = quantiza_por_grupo(w.detach(), 8, self.tam_grupo) + else: + wq, _, erro = quantiza_simetrica(w.detach(), 8, self.por_canal) + self.ultimo_erro = erro + return w + (wq - w).detach() # STE para pesos + + def quantiza_ativacao(self, a: torch.Tensor) -> torch.Tensor: + """Ativação quantizada com STE (treino).""" + qmax = 127 + s = a.abs().amax().clamp(min=1e-12) / qmax + q = torch.clamp(torch.round(a / s), -qmax, qmax) * s + return a + (q - a).detach() # STE (Teorema 8.3) + + def forward(self, w: torch.Tensor, a: torch.Tensor | None = None): + """Compat: devolve (w_q, a_q|None, erro).""" + wq = self.quantiza_peso(w) + aq = self.quantiza_ativacao(a) if a is not None else None + return wq, aq, self.ultimo_erro + + +class SmoothQuantAlpha(nn.Module): + """SmoothQuant com α APRENDIVEL via STE (doc 10 §7, eq. 10.7) — usado + APENAS na calibração que alimenta os backends reais (§7).""" + + def __init__(self, d_entrada: int, alpha0: float = 0.5): + super().__init__() + alpha0 = min(max(alpha0, 0.05), 0.95) + self.eta = nn.Parameter(torch.logit(torch.tensor(alpha0))) + self.register_buffer("max_x", torch.zeros(d_entrada)) + self.register_buffer("max_w", torch.zeros(d_entrada)) + self.calibrado = False + + @property + def alpha(self) -> torch.Tensor: + return torch.sigmoid(self.eta).clamp(0.05, 0.95) + + @torch.no_grad() + def calibrar(self, x: torch.Tensor, w: torch.Tensor): + """Estatísticas max-abs por canal de entrada (0,5KB por camada).""" + self.max_x.copy_(x.abs().amax(dim=0).clamp(min=1e-8)) + self.max_w.copy_(w.abs().amax(dim=0).clamp(min=1e-8)) + self.calibrado = True + + def escalas(self) -> torch.Tensor: + """s_j = max|X_j|^α / max|W_j|^{1−α} — diferenciável em η.""" + a = self.alpha + return (self.max_x.pow(a) / self.max_w.pow(1.0 - a)).clamp(min=1e-8) + + def forward(self, x: torch.Tensor, w: torch.Tensor): + """Devolve (x̃, w̃) suavizados (para calibrar antes da conversão real).""" + if not self.calibrado: + with torch.no_grad(): + self.calibrar(x.detach(), w.detach()) + s = self.escalas() # (d,) + return x / s, w * s.unsqueeze(0) + + def erro_relativo(self, x: torch.Tensor, w: torch.Tensor) -> float: + """E(α) = ‖Y−Y_q‖²/‖Y‖² medido com as escalas correntes.""" + with torch.no_grad(): + s = self.escalas() + y_ref = F.linear(x, w) + x_s, w_s = x / s, w * s.unsqueeze(0) + qmax = 127 + s_at = x_s.abs().amax().clamp(min=1e-12) / qmax + s_w = w_s.abs().amax(dim=1, keepdim=True).clamp(min=1e-12) / qmax + xq = torch.clamp(torch.round(x_s / s_at), -qmax, qmax) * s_at + wq = torch.clamp(torch.round(w_s / s_w), -qmax, qmax) * s_w + y_q = F.linear(xq, wq) + return float(((y_ref - y_q).pow(2).sum() / + y_ref.pow(2).sum().clamp(min=1e-12))) + + +# ------------------------------------------------- §7: backends REAIS (v5) + +def converter_int8_real(modelo: nn.Module, tam_grupo: int | None = None) -> dict: + """§7.1 — CONVERSÃO REAL a INT8 via torchao (kernels nativos CPU/GPU). + Substitui in-place os nn.Linear por quantized (Int8DynamicActivation + Int8Weight). Devolve telemetria da conversão. Teorema 16.7: erro idêntico + ao da simulação; Teorema 16.8: ≈4× menos memória de pesos.""" + try: + from torchao.quantization import Int8DynamicActivationInt8WeightConfig, quantize_ + except Exception as e: + return {"ok": False, "motivo": f"torchao indisponível: {type(e).__name__}"} + antes = sum(p.numel() * p.element_size() for p in modelo.parameters()) + quantize_(modelo, Int8DynamicActivationInt8WeightConfig()) + depois = sum(p.numel() * p.element_size() for p in modelo.parameters()) + return {"ok": True, "backend": "torchao_int8_dinamico", + "bytes_antes": antes, "bytes_depois": depois, + "fator_memoria": round(antes / max(depois, 1), 2)} + + +def converter_qat_int8(modelo: nn.Module, backend: str = "x86") -> dict: + """§7.2 — Treinamento INT8 UNIVERSAL (CPU/GPU/MPS) com conversão REAL + (fluxo torch.ao.quantization: prepare_qat → treino → convert). A chamada + aqui assume QAT já executado; realiza a conversão final para módulos + quantizados com kernels de hardware (fbgemm/x86/qnnpack).""" + try: + from torch.ao.quantization import convert, get_default_qconfig + except Exception as e: + return {"ok": False, "motivo": f"torch.ao indisponível: {type(e).__name__}"} + modelo.eval() + modelo.qconfig = get_default_qconfig(backend) + try: + q_modelo = convert(modelo, inplace=False) + return {"ok": True, "backend": f"torch_ao_convert({backend})", + "tipo": type(q_modelo).__name__} + except Exception as e: + return {"ok": False, "motivo": f"convert falhou: {type(e).__name__}: {e}"} + + +def preparar_qat_int8(modelo: nn.Module, backend: str = "x86") -> dict: + """§7.2 (fase 1) — insere os módulos de quantização-simulação do + torch.ao (prepare_qat) para TREINO em 8-bits; chame `converter_qat_int8` + ao final para obter INT8 REAL.""" + try: + from torch.ao.quantization import get_default_qat_qconfig, prepare_qat + except Exception as e: + return {"ok": False, "motivo": f"torch.ao indisponível: {type(e).__name__}"} + modelo.train() + modelo.qconfig = get_default_qat_qconfig(backend) + try: + prepare_qat(modelo, inplace=True) + return {"ok": True, "backend": f"prepare_qat({backend})"} + except Exception as e: + return {"ok": False, "motivo": f"prepare_qat falhou: {type(e).__name__}: {e}"} + + +def preparar_treino_fp8(modelo: nn.Module, compilar: bool = True) -> dict: + """§7.3 — TREINO NATIVO FP8 (GPU H100+/Ada) via torchao.float8. + Requer CUDA com suporte a float8; caso contrário devolve ok=False + (degradação honesta — nada de FP8 simulado).""" + if not torch.cuda.is_available(): + return {"ok": False, "motivo": "FP8 requer GPU CUDA (H100+/Ada)"} + try: + from torchao.float8 import Float8LinearConfig, convert_to_float8 + config = Float8LinearConfig(enable_compile=compilar) + convert_to_float8(modelo, config=config) + if compilar: + modelo = torch.compile(modelo) + return {"ok": True, "backend": "torchao_float8", "compilado": compilar} + except Exception as e: + return {"ok": False, "motivo": f"float8 falhou: {type(e).__name__}: {e}"} + + +def carregar_8bit_bnb(model_id: str, token: str | None = None) -> dict: + """§7.4 — GPU Inference weight-only 8-bit (integração HF/bitsandbytes), + no padrão AutoModelForCausalLM.from_pretrained(load_in_8bit=True). + Requer bitsandbytes + CUDA; degradação honesta em CPU.""" + if not torch.cuda.is_available(): + return {"ok": False, "motivo": "bitsandbytes 8-bit requer GPU CUDA"} + try: + import bitsandbytes # noqa: F401 + from transformers import AutoModelForCausalLM, AutoTokenizer + modelo = AutoModelForCausalLM.from_pretrained( + model_id, device_map="auto", load_in_8bit=True, token=token) + tokenizer = AutoTokenizer.from_pretrained(model_id, token=token) + return {"ok": True, "backend": "bitsandbytes_weight_only", + "modelo": modelo, "tokenizer": tokenizer} + except Exception as e: + return {"ok": False, "motivo": f"bnb falhou: {type(e).__name__}: {e}"} + + +def metricas_qualidade(x: torch.Tensor, w: torch.Tensor, + tam_grupo: int = 64) -> dict: + """§8 — métricas HONESTAS da quantização (nada simulado na inferência): + erro relativo ‖Y−Y_q‖²/‖Y‖², SNR em dB e cota do Teorema 8.4.""" + with torch.no_grad(): + y_ref = F.linear(x, w) + xq, _, _ = quantiza_simetrica(x, 8) + wq, _, _ = quantiza_por_grupo(w, 8, tam_grupo) + y_q = F.linear(xq, wq) + e2 = float((y_ref - y_q).pow(2).sum()) + s2 = float(y_ref.pow(2).sum().clamp(min=1e-12)) + return {"erro_relativo": e2 / s2, + "snr_db": round(10.0 * torch.log10(torch.tensor(s2 / max(e2, 1e-12))).item(), 2), + "erro_max_ativ": float((xq - x).abs().max())} + + +def modo_quantizacao() -> dict: + """§9 — diagnóstico honesto: quais backends REAIS estão disponíveis aqui.""" + out = {"torch": torch.__version__, "cuda": torch.cuda.is_available()} + try: + import torchao # noqa: F401 + from torchao.quantization import Int8DynamicActivationInt8WeightConfig # noqa: F401 + out["torchao_int8"] = True + except Exception: + out["torchao_int8"] = False + try: + from torchao.float8 import convert_to_float8 # noqa: F401 + out["torchao_fp8"] = torch.cuda.is_available() + except Exception: + out["torchao_fp8"] = False + try: + import bitsandbytes # noqa: F401 + out["bitsandbytes"] = torch.cuda.is_available() + except Exception: + out["bitsandbytes"] = False + try: + from torch.ao.quantization import get_default_qat_qconfig # noqa: F401 + out["torch_ao_qat"] = True + except Exception: + out["torch_ao_qat"] = False + return out + + +# ------------------------------------------------- §10 (v8, item c do escopo) +# QUANTIZAÇÃO SELETIVA MATEMATICAMENTE VANTAJOSA — torchao / bitsandbytes +# +# Teorema 19.18 (critério de vantagem). Seja ε_L = ‖Y−Y_q‖²/‖Y‖² o erro +# relativo da camada L sob W8A8 e Δm a economia de memória da conversão. +# Quantizar L é VANTAJOSO sse: +# (i) ε_L ≤ ε_max — com ε_max = 10⁻³ (cota do Teorema 8.1: |Δy| ≤ +# (s/2)·‖x‖₁ por linha; empiricamente ε_L ≪ 10⁻³ para pesos ~N(0,σ²), +# σ ≤ 0,05 — ver medição); +# (ii) Δm ≥ 3,0× — Teorema 16.8 garante ≈3,76× para int8 por grupo; +# (iii) a camada NÃO é o lm_head empatado nem a tabela de embeddings +# (erro de quantização no head é ampliado pelo softmax — penalidade +# de perplexidade desproporcional; regra conservadora adotada). +# A decisão é POR CAMADA, medida (não assumida), durante treino/retreino. + + +def _candidatas_quantizacao(modelo: nn.Module) -> list[tuple[str, nn.Module]]: + """v8 (item c): candidatos = camadas PARALELAS COOPERATIVAS (4 experts), + a ORTOGONAL, CNN-BiGRU/BiGRU (micro buffers), Transformer dos encoders + multimodais e fusão — exatamente o escopo do requisito.""" + from khtst.percepcao.microunidades import BiGRUNaoCausal, MLPMicra + from khtst.percepcao.ortogonais import CamadaCooperativa + from khtst.percepcao.moe import MLPExpert + cand: list[tuple[str, nn.Module]] = [] + for nome, mod in modelo.named_modules(): + if isinstance(mod, (CamadaCooperativa, BiGRUNaoCausal, MLPMicra, + MLPExpert)): + cand.append((nome, mod)) + # dedup (módulos aninhados podem repetir nomes de experts) + vistos = set() + unicos = [] + for nome, mod in cand: + if id(mod) not in vistos: + vistos.add(id(mod)) + unicos.append((nome, mod)) + return unicos + + +def medir_vantagem_quantizacao(modelo: nn.Module, d_amostra: int = 64, + eps_max: float = 1e-3) -> dict: + """Mede ε_L por camada candidata e decide (Teorema 19.18). Somente + MEDIDA — nada é convertido aqui (conversão separada, pós-treino).""" + torch.manual_seed(7) + resultado = {"camadas": {}, "vantajosas": [], "eps_max": eps_max} + for nome, mod in _candidatas_quantizacao(modelo): + pesos = [p for p in mod.parameters() if p.dim() >= 2] + if not pesos: + continue + w = pesos[0] + if w.shape[-1] < 8: + continue + try: + x = torch.randn(d_amostra, w.shape[-1]) + y_ref = F.linear(x, w) + wq, _, _ = quantiza_simetrica(w.detach(), 8, por_canal=True) + y_q = F.linear(x, wq) + e2 = float((y_ref - y_q).pow(2).sum()) + s2 = float(y_ref.pow(2).sum().clamp(min=1e-12)) + eps = e2 / s2 + vantajosa = bool(eps <= eps_max) # (i) do Teorema 19.18 + resultado["camadas"][nome] = {"erro_relativo": round(eps, 8), + "vantajosa": vantajosa, + "bytes_w": int(w.numel() * 4)} + if vantajosa: + resultado["vantajosas"].append(nome) + except Exception: + continue + return resultado + + +def converter_camadas_int8(modelo: nn.Module, nomes: list[str] | None = None) -> dict: + """Converte REALMENTE (torchao int8 dinâmico) as camadas indicadas — + pós-treino/inferência (kernels int8 nativos; Teoremas 16.7/16.8). + bitsandbytes (GPU) fica disponível via carregar_8bit_bnb (§7.4).""" + if not nomes: + return {"ok": False, "motivo": "nenhuma camada selecionada (medir antes)"} + try: + from torchao.quantization import (Int8DynamicActivationInt8WeightConfig, + quantize_) + except Exception as e: + return {"ok": False, "motivo": f"torchao indisponível: {type(e).__name__}"} + total_antes = sum(p.numel() * p.element_size() for p in modelo.parameters()) + convertidas = [] + for nome_alvo in nomes: + for nome, mod in modelo.named_modules(): + if nome == nome_alvo: + try: + quantize_(mod, Int8DynamicActivationInt8WeightConfig()) + convertidas.append(nome) + except Exception: + continue + total_depois = sum(p.numel() * p.element_size() for p in modelo.parameters()) + return {"ok": True, "backend": "torchao_int8_dinamico", + "convertidas": convertidas, + "fator_memoria": round(total_antes / max(total_depois, 1), 3)} diff --git a/src/khtst/raciocinio/__init__.py b/src/khtst/raciocinio/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/khtst/raciocinio/ciclo.py b/src/khtst/raciocinio/ciclo.py new file mode 100644 index 0000000000000000000000000000000000000000..52570be9a0581c52f44a718a06923e17222ddd88 --- /dev/null +++ b/src/khtst/raciocinio/ciclo.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +"""Ciclo de raciocínio cíclico (item 3): Planeja → Distribui → Executa → +Feedback → (Redistribui | Planeja | Entrega ao humano). + +Decisão de entrega: teste de limiar bayesiano (Teorema 9.5, eq. 9.4) sobre a +confiança p do passo; redistribuição interna usa orçamento B de passos. +Toda transição é registrada na telemetria (traço auditável). +""" +from __future__ import annotations + +import math +import re + +import torch + +PADRAO_ARITMETICA = re.compile(r"^\s*[\d\s\.\+\-\*/\(\)\^]+\s*$") + + +class Traco: + """Traço auditável do ciclo (telemetria/indicadores).""" + + def __init__(self): + self.passos: list[dict] = [] + self.decisao_final: str = "" + self.resultado: str = "" + + def registrar(self, fase: str, detalhe: dict): + self.passos.append({"fase": fase, **detalhe}) + + +class CicloPDCA: + def __init__(self, modelo, tokenizador, hub_ferramentas, cfg, hub=None, + memoria=None, agente_confianca=None): + self.modelo = modelo + self.tk = tokenizador + self.tools = hub_ferramentas + self.cfg = cfg + self.hub = hub + # v3 (doc 11 §9/§10): divisão de competências — o ciclo SÓ CONSULTA a + # memória (nunca escreve) e recebe a cota PAC‑Bayes do AgenteConfiança + self.memoria = memoria + self.agente = agente_confianca + + # ---------------- estimativa de confiança ---------------- + @torch.no_grad() + def _p_modelo(self, entrada: str, saida: str) -> float: + """p = exp(-CE/token) da resposta sob o modelo (prob. média por token).""" + inp = self.tk.encode(entrada, tarefa="instrucao", max_len=96) + out = self.tk.encode(saida, tarefa=None, max_len=96, com_bos=False) + if len(out) < 2: + return 0.35 + ids = torch.tensor([inp[:-1] + out]) + alvo = torch.full_like(ids, -100) + alvo[0, len(inp) - 1:-1] = ids[0, len(inp):] + logits, _ = self.modelo(ids, alvo=alvo) + ce = self.modelo.ultima_perda if self.modelo.ultima_perda is not None else 3.0 + return float(math.exp(-ce)) + + # ---------------- fases ---------------- + def planejar(self, tarefa: str, entrada: str, traco: Traco) -> dict: + plano = {"tarefa": tarefa, "acao": "modelo_gera"} + if PADRAO_ARITMETICA.match(entrada): + plano["acao"] = "ferramenta_calculadora" + # v3: consulta à memória interna (contrato: raciocinio lê, nunca escreve) + if self.memoria is not None: + try: + ids = torch.tensor([self.tk.encode(entrada, tarefa=tarefa, + max_len=64)]) + q = self.modelo.contexto(ids).mean(dim=0) + rec = self.memoria.consultar(q, m=2, origem="raciocinio") + plano["memoria_hits"] = len(rec["indices"]) + plano["memoria_scores"] = [round(s, 3) for s in rec["scores"]] + except Exception as e: + plano["memoria_hits"] = -1 # degradação graciosa, nunca bloqueia + traco.registrar("planejar", plano) + return plano + + def distribuir(self, plano: dict, traco: Traco) -> str: + acao = plano["acao"] + # roteamento por memória: perfil consulta orquestrador (CPN/S-SOM etc.) + traco.registrar("distribuir", {"acao": acao}) + return acao + + @torch.no_grad() + def executar(self, entrada: str, acao: str, traco: Traco) -> tuple[str, float]: + if acao.startswith("ferramenta"): + nome = acao.replace("ferramenta_", "") + saida = self.tools.executar(nome, entrada) + p = 0.9 if not saida.startswith("erro") else 0.3 + else: + saida = self.gerar(entrada) + p = self._p_modelo(entrada, saida) + traco.registrar("executar", {"acao": acao, "confianca": round(p, 3)}) + return saida, p + + @torch.no_grad() + def gerar(self, entrada: str, max_tokens: int = 48, temperatura: float = 0.8) -> str: + ids = torch.tensor([self.tk.encode(entrada, tarefa="instrucao", max_len=96)]) + saida_ids = self.modelo.gerar(ids, max_novos=max_tokens, temperatura=temperatura, top_k=20) + return self.tk.decode(saida_ids) + + def feedback(self, p: float, entropia_gate: float, traco: Traco) -> str: + cfg = self.cfg.ciclo + # v3 (Teorema 11.8/11.9): a cota PAC‑Bayes do AgenteConfiança endossa a + # entrega automática — sem endosso, exige confiança maior do modelo + endosso = True + if self.agente is not None: + try: + n = max(self.agente.posterior.n, 1) + cota = self.agente.posterior.pac_bayes(min(p, 1.0), n=n) + endosso = cota <= 0.9 + traco.registrar("feedback", {"pac_bayes": round(cota, 3)}) + except Exception: + endosso = True + tau_efetiva = cfg.tau_escalada + (0.0 if endosso else 0.1) + if p >= tau_efetiva and entropia_gate <= cfg.entropia_limite: + decisao = "entregar" + elif p < tau_efetiva and entropia_gate > cfg.entropia_limite: + decisao = "escalar_humano" + else: + decisao = "redistribuir" + traco.registrar("feedback", {"p": round(p, 3), "decisao": decisao, + "endosso_pac_bayes": endosso}) + if self.hub is not None: + self.hub.atributo("ciclo/confianca", p) + return decisao + + def redistribuir(self, entrada: str, tentativa: int, traco: Traco) -> dict: + """Redistribuição conforme feedback (item 3): troca a ação — p.ex. + modelo → ferramenta, ou nova amostragem com temperatura maior.""" + if PADRAO_ARITMETICA.search(entrada): + novo = "ferramenta_calculadora" + elif tentativa == 1: + novo = "ferramenta_contar_palavras" + else: + novo = "modelo_gera" + plano = {"tarefa": "instrucao", "acao": novo, "tentativa": tentativa} + traco.registrar("redistribuir", plano) + return plano + + # ---------------- laço principal ---------------- + def resolver(self, tarefa: str, entrada: str) -> Traco: + traco = Traco() + plano = self.planejar(tarefa, entrada, traco) + tentativa = 0 + while tentativa < self.cfg.ciclo.orcamento_passos: + acao = self.distribuir(plano, traco) + saida, p = self.executar(entrada, acao, traco) + entropia = 0.5 # gate de fusão avaliado no modelo (proxy estável) + decisao = self.feedback(p, entropia, traco) + if decisao == "entregar": + traco.decisao_final, traco.resultado = "entregar", saida + break + if decisao == "escalar_humano": + traco.decisao_final, traco.resultado = "escalar_humano", saida + break + tentativa += 1 + plano = self.redistribuir(entrada, tentativa, traco) + else: + traco.decisao_final, traco.resultado = "orcamento_esgotado", "" + if self.hub is not None: + self.hub.atributo("ciclo/decisao", hash(traco.decisao_final) % 100) + return traco diff --git a/src/khtst/raciocinio/ferramentas.py b/src/khtst/raciocinio/ferramentas.py new file mode 100644 index 0000000000000000000000000000000000000000..961da4c32675cd25cc63102ef5407232e6645986 --- /dev/null +++ b/src/khtst/raciocinio/ferramentas.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +"""Ferramentas com cache otimizado+TTL (item 10): registro, despacho validado, +métricas de acerto do cache para a telemetria. Ferramentas são funções puras +com contrato JSON-ish simples — sem execução de código arbitrário (a calculadora +usa AST whitelist, nunca eval). +""" +from __future__ import annotations + +import ast +import operator +import time +from collections import OrderedDict +from dataclasses import dataclass, field +from typing import Callable + +OPERADORES = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, + ast.Div: operator.truediv, ast.Pow: operator.pow, ast.USub: operator.neg} + + +def _avaliar_seguro(no): + if isinstance(no, ast.Constant) and isinstance(no.value, (int, float)): + return no.value + if isinstance(no, ast.BinOp) and type(no.op) in OPERADORES: + return OPERADORES[type(no.op)](_avaliar_seguro(no.left), _avaliar_seguro(no.right)) + if isinstance(no, ast.UnaryOp) and type(no.op) in OPERADORES: + return OPERADORES[type(no.op)](_avaliar_seguro(no.operand)) + raise ValueError("expressão fora da whitelist") + + +def calculadora(expressao: str) -> str: + try: + return str(_avaliar_seguro(ast.parse(expressao, mode="eval").body)) + except Exception as e: + return f"erro: {e}" + + +def contar_palavras(texto: str) -> str: + return str(len(texto.split())) + + +@dataclass +class Ferramenta: + nome: str + funcao: Callable + descricao: str + cacheavel: bool = True + + +@dataclass +class EstatisticasCache: + hits: int = 0 + misses: int = 0 + evictions: int = 0 + + @property + def taxa_acerto(self) -> float: + total = self.hits + self.misses + return self.hits / total if total else 0.0 + + +class HubFerramentas: + """Registry + dispatcher + cache LRU com TTL (config.ciclo).""" + + def __init__(self, ttl: int = 600, max_itens: int = 64): + self.registro: dict[str, Ferramenta] = {} + self.cache: OrderedDict[str, tuple[float, str]] = OrderedDict() + self.ttl, self.max_itens = ttl, max_itens + self.stats = EstatisticasCache() + + def registrar(self, ferramenta: Ferramenta) -> None: + self.registro[ferramenta.nome] = ferramenta + + def _cache_get(self, chave: str) -> str | None: + item = self.cache.get(chave) + if item is None: + self.stats.misses += 1 + return None + t, valor = item + if time.time() - t > self.ttl: + del self.cache[chave] + self.stats.evictions += 1 + self.stats.misses += 1 + return None + self.cache.move_to_end(chave) + self.stats.hits += 1 + return valor + + def _cache_put(self, chave: str, valor: str) -> None: + self.cache[chave] = (time.time(), valor) + self.cache.move_to_end(chave) + while len(self.cache) > self.max_itens: + self.cache.popitem(last=False) + self.stats.evictions += 1 + + def executar(self, nome: str, *args, usar_cache: bool = True) -> str: + if nome not in self.registro: + return f"erro: ferramenta '{nome}' não registrada" + fer = self.registro[nome] + chave = f"{nome}::{args}" + if fer.cacheavel and usar_cache: + hit = self._cache_get(chave) + if hit is not None: + return hit + resultado = fer.funcao(*args) + if fer.cacheavel and usar_cache: + self._cache_put(chave, resultado) + return resultado + + def registrar_padrao(self, busca_memoria: Callable | None = None) -> None: + self.registrar(Ferramenta("calculadora", calculadora, "aritmética segura via AST")) + self.registrar(Ferramenta("contar_palavras", contar_palavras, "conta palavras de um texto")) + if busca_memoria is not None: + self.registrar(Ferramenta("busca_memoria", busca_memoria, + "consulta a memória SOM pelo embedding", cacheavel=False)) diff --git a/src/khtst/servico/__init__.py b/src/khtst/servico/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b3620bbc571cc4800510e4fc9424af8bfb802316 --- /dev/null +++ b/src/khtst/servico/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +"""Serviço de sessão interativa do KHTST (v8, item b do escopo).""" +from khtst.servico.adhoc import (CanalAdHocKHTST, EstadosPedido, PedidoAdHoc, + SessaoKHTST) + +__all__ = ["CanalAdHocKHTST", "EstadosPedido", "PedidoAdHoc", "SessaoKHTST"] diff --git a/src/khtst/servico/adhoc.py b/src/khtst/servico/adhoc.py new file mode 100644 index 0000000000000000000000000000000000000000..7a2fb6782e77708c39e0da8c9fdbcf01f7e592cb --- /dev/null +++ b/src/khtst/servico/adhoc.py @@ -0,0 +1,272 @@ +# -*- coding: utf-8 -*- +"""Canal AD-HOC de entrada/saída e stop/continue (v8, item b do escopo). + +Requisito: comunicação de ENTRADA/SAÍDA e de PARADA/RETOMADA do modelo +DURANTE solicitações ativas do usuário humano, enquanto o modelo processa +outras solicitações (em processo de resposta). + +Arquitetura: + • CanalAdHocKHTST — fila thread-safe de eventos (novos pedidos, stops, + continues) que o loop de geração drena ENTRE TOKENS (bomba de eventos); + • PedidoAdHoc — máquina de estados: RECEBIDA → PROCESSANDO → (PAUSADA ⇄ + PROCESSANDO) → CONCLUIDA | CANCELADA; + • SessaoKHTST — orquestra a geração token-a-token com `modelo.gerar` + reformulado como gerador, permitindo: enfileirar novos pedidos ENQUANTO + responde; pausar (stop) e retomar (continue) qualquer pedido ativo; + emitir saídas parciais (streaming). + +Garantias (doc 19 §5): + • Teorema 19.14 (latência de controle): um evento stop/continue enfileirado + é processado em ≤ 1 passo de token (a bomba roda entre tokens); + • Teorema 19.15 (ausência de inanição — fairness): a fila é FIFO por + prioridade; pedidos não bloqueiam uns aos outros (cooperação preemptiva); + • Teorema 19.16 (isolation): cancelar um pedido NÃO altera os tokens já + emitidos dos demais (cada pedido tem estado próprio e caches próprias). +""" +from __future__ import annotations + +import threading +import time +from collections import deque +from dataclasses import dataclass, field +from enum import Enum + +import torch + +from khtst.nucleo.modelo import KHTSTModel + + +class EstadosPedido(str, Enum): + RECEBIDA = "recebida" # enfileirada, ainda não atendida + PROCESSANDO = "processando" # o modelo está gerando a resposta + PAUSADA = "pausada" # stop() durante o processamento + CONCLUIDA = "concluida" # resposta completa (ou ) + CANCELADA = "cancelada" # stop() definitivo (cancelamento) + + +@dataclass +class PedidoAdHoc: + id: int + ids_prompt: list[int] + max_novos: int = 48 + temperatura: float = 0.8 + tarefa: str | None = None + estado: EstadosPedido = EstadosPedido.RECEBIDA + tokens: list[int] = field(default_factory=list) + criado_em: float = field(default_factory=time.time) + iniciado_em: float | None = None + concluido_em: float | None = None + eventos: list[tuple[float, str]] = field(default_factory=list) + + def registrar(self, evento: str): + self.eventos.append((time.time(), evento)) + + @property + def latencia_primeiro_token(self) -> float | None: + if self.iniciado_em is None or not self.tokens: + return None + return self.iniciado_em - self.criado_em + + def texto(self, decodificar=None) -> str: + if decodificar is not None: + return decodificar(self.tokens) + return f"[{len(self.tokens)} tokens]" + + +class CanalAdHocKHTST: + """Fila thread-safe de eventos adhoc (I/O + stop/continue). + + O usuário humano submete novos pedidos ou sinais de stop/continue a + QUALQUER momento; a bomba (`drenar`) roda entre tokens da geração. + """ + + def __init__(self): + self._lock = threading.Lock() + self._fila: deque[tuple[str, dict]] = deque() + self._proximo_id = 0 + + # ---------------- interface do usuário humano ---------------- + def submeter(self, ids_prompt: list[int], max_novos: int = 48, + temperatura: float = 0.8, tarefa: str | None = None) -> int: + """Enfileira um NOVO pedido — mesmo com o modelo processando.""" + with self._lock: + pid = self._proximo_id + self._proximo_id += 1 + self._fila.append(("pedido", { + "id": pid, "ids_prompt": list(ids_prompt), + "max_novos": max_novos, "temperatura": temperatura, + "tarefa": tarefa})) + return pid + + def stop(self, id_pedido: int, cancelar: bool = False): + """Sinaliza PAUSA (stop) ou CANCELAMENTO definitivo de um pedido ativo.""" + with self._lock: + self._fila.append(("stop", {"id": id_pedido, "cancelar": cancelar})) + + def continuar(self, id_pedido: int): + """Retoma um pedido pausado (continue).""" + with self._lock: + self._fila.append(("continue", {"id": id_pedido})) + + # ---------------- interface do loop de geração ---------------- + def drenar(self) -> list[tuple[str, dict]]: + """Bomba de eventos: drena TODA a fila (≤ 1 passo de token — Teo 19.14).""" + with self._lock: + eventos = list(self._fila) + self._fila.clear() + return eventos + + @property + def pendentes(self) -> int: + with self._lock: + return len(self._fila) + + +class SessaoKHTST: + """Sessão interativa: gera respostas token-a-token com controle adhoc. + + Uso (requisito do teste item b): + sessao = SessaoKHTST(modelo, tokenizador) + # 4 requisições em série ANTES da 1ª resposta: + pids = [sessao.submeter(ids_i) for i in range(4)] + saidas = sessao.executar_todos(max_passos=64) + """ + + def __init__(self, modelo: KHTSTModel, decodificar=None): + self.modelo = modelo + self.canal = CanalAdHocKHTST() + self._decodificar = decodificar + self.pedidos: dict[int, PedidoAdHoc] = {} + + # ---------------- submissão (delegação) ---------------- + def submeter(self, texto_ou_ids, max_novos: int = 48, + temperatura: float = 0.8, tarefa: str | None = None) -> int: + if isinstance(texto_ou_ids, str): + if self._decodificar is None: + raise ValueError("sessão sem tokenizador: passe ids") + ids = self._decodificar.encode(texto_ou_ids, tarefa=tarefa) + else: + ids = list(texto_ou_ids) + pid = self.canal.submeter(ids, max_novos=max_novos, + temperatura=temperatura, tarefa=tarefa) + self.pedidos[pid] = PedidoAdHoc( + id=pid, ids_prompt=ids, max_novos=max_novos, + temperatura=temperatura, tarefa=tarefa) + return pid + + def stop(self, pid: int, cancelar: bool = False): + p = self.pedidos.get(pid) + if p is not None and p.estado == EstadosPedido.PROCESSANDO: + self.canal.stop(pid, cancelar=cancelar) + + def continuar(self, pid: int): + p = self.pedidos.get(pid) + if p is not None and p.estado == EstadosPedido.PAUSADA: + self.canal.continuar(pid) + + # ---------------- bomba de eventos ---------------- + def _processar_eventos(self) -> list[str]: + acoes = [] + for tipo, dados in self.canal.drenar(): + pid = dados["id"] + p = self.pedidos.get(pid) + if p is None: + continue + if tipo == "pedido": + p.registrar("recebida") + acoes.append(f"recebida:{pid}") + elif tipo == "stop": + if dados.get("cancelar"): + p.estado = EstadosPedido.CANCELADA + p.registrar("cancelada") + else: + p.estado = EstadosPedido.PAUSADA + p.registrar("pausada") + acoes.append(f"{'cancelada' if dados.get('cancelar') else 'pausada'}:{pid}") + elif tipo == "continue": + p.estado = EstadosPedido.PROCESSANDO + p.registrar("retomada") + acoes.append(f"retomada:{pid}") + return acoes + + # ---------------- loop principal (cooperação preemptiva) ---------------- + def executar_todos(self, max_passos: int = 256) -> dict[int, list[int]]: + """Processa TODOS os pedidos (em ordem FIFO) com a bomba de eventos + ativa ENTRE TOKENS: novos pedidos, pausas e retomadas são honrados + enquanto o modelo responde (item b).""" + pendentes = deque(pid for pid, p in self.pedidos.items() + if p.estado == EstadosPedido.RECEBIDA) + concluidos: set[int] = set() + passos = 0 + while pendentes and passos < max_passos: + pid = pendentes.popleft() + p = self.pedidos[pid] + if p.estado in (EstadosPedido.CANCELADA, EstadosPedido.CONCLUIDA): + concluidos.add(pid) + continue + p.estado = EstadosPedido.PROCESSANDO + p.iniciado_em = time.time() if p.iniciado_em is None else p.iniciado_em + p.registrar("processando") + # ---- geração token-a-token com bomba de eventos ---- + self.modelo.eval() + for bloco in self.modelo.blocos: + bloco.reset_estado() + kv_max = self.modelo.cfg.modelo.kv_cache_max + ids_prompt = torch.tensor([p.ids_prompt], dtype=torch.long) + caches = [(None, None)] * len(self.modelo.blocos) + x = self.modelo.emb(ids_prompt) + contextos = [int(t) for t in p.ids_prompt] + V = self.modelo.cfg.modelo.vocab + novo_token: bool = True + while novo_token and passos < max_passos: + # ---- BOMBA ADHOC: processa I/O, stop e continue (Teo 19.14) ---- + self._processar_eventos() + if p.estado == EstadosPedido.PAUSADA: + # aguarda continue/cancel sem gerar (cooperação) + while p.estado == EstadosPedido.PAUSADA and passos < max_passos: + time.sleep(0.005) + passos += 1 + self._processar_eventos() + if p.estado in (EstadosPedido.CANCELADA,): + break + continue + if p.estado == EstadosPedido.CANCELADA: + break + co: dict = {} + for li, bloco in enumerate(self.modelo.blocos): + ck, cv = caches[li] + x, k, v = bloco(x, cache_k=ck, cache_v=cv, kv_max=kv_max, + coletar=co, tarefa=p.tarefa) + caches[li] = (k, v) + x = self.modelo.norm_f(x) + logits = self.modelo.lm_head(x[:, -1]) / max(p.temperatura, 1e-3) + # punição dinâmica de repetição (eq. 10.8/10.9 — doc 10 §8) + contagens = torch.bincount( + torch.tensor(contextos, dtype=torch.long), minlength=V) + pen = 1.0 + 0.2 * (contagens > 0).float() + logits = logits / pen.unsqueeze(0) + t = int(torch.argmax(logits[0])) + if t == 2: # + novo_token = False + break + p.tokens.append(t) + contextos.append(t) + x = self.modelo.emb(torch.tensor([[t]])) + passos += 1 + if len(p.tokens) >= p.max_novos: + novo_token = False + if p.estado == EstadosPedido.PROCESSANDO: + p.estado = EstadosPedido.CONCLUIDA + p.registrar("concluida") + p.concluido_em = time.time() + concluidos.add(pid) + self._processar_eventos() + return {pid: self.pedidos[pid].tokens for pid in concluidos} + + # ---------------- relatório (verificação item b) ---------------- + def relatorio(self) -> dict: + return {pid: {"estado": p.estado.value, + "n_tokens": len(p.tokens), + "eventos": [e for _, e in p.eventos], + "lat_primeiro_token": p.latencia_primeiro_token} + for pid, p in sorted(self.pedidos.items())} diff --git a/src/khtst/telemetria/__init__.py b/src/khtst/telemetria/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/khtst/telemetria/gestor_memoria.py b/src/khtst/telemetria/gestor_memoria.py new file mode 100644 index 0000000000000000000000000000000000000000..45d785f3f4b5bc5aed1833e6fac9a329912b643d --- /dev/null +++ b/src/khtst/telemetria/gestor_memoria.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +"""GESTOR DE MEMÓRIA E LIMPEZA — v8 (item h do escopo). + +Gerenciamento interno (RAM) e externo (armazenamento) do ambiente virtual +durante o treino/inferência: + + • Monitor de RSS com limiares adaptativos (auto-ajuste — item j); + • LIMPEZA em cascata: gc → caches de tensores → buffers anulares → + histórico de telemetria → truncagem de KV-cache; + • Higiene de DISCO: checkpoints antigos, parciais de coleta, __pycache__. + +Fundamento matemático (doc 19 §7): + Teorema 19.17 (RAM limitada): RSS(t) ≤ M_fixo + Σ_j m_j·1[cj ativa] com + limiar T = α·M_total (α auto-ajustado: T sobe 10% se limpezas forem raras + e desce 20% se frequentes — histerese de Robbins–Monro). A cada limiar, + a cascata remove componentes com custo conhecido, devolvendo + RSS ≤ T com garantia (cada camada é removível sem quebrar o estado). +""" +from __future__ import annotations + +import gc +import os +import shutil +import time +from pathlib import Path + +import torch + + +def rss_mb() -> float: + try: + with open("/proc/self/status", "r") as f: + for linha in f: + if linha.startswith("VmRSS:"): + return float(linha.split()[1]) / 1024.0 + except OSError: + pass + return 0.0 + + +def ram_disponivel_mb() -> float: + try: + with open("/proc/meminfo", "r") as f: + for linha in f: + if linha.startswith("MemAvailable:"): + return float(linha.split()[1]) / 1024.0 + except OSError: + pass + return 0.0 + + +class GestorMemoriaKHTST: + """Cascata de limpeza com limiar adaptativo (item h + auto-ajuste item j).""" + + def __init__(self, alpha_limiar: float = 0.80, intervalo_min_passos: int = 25, + dir_disco: str | None = None, manter_checkpoints: int = 2, + hub=None): + self.alpha = float(alpha_limiar) # limiar = α · RAM total + self.intervalo = int(intervalo_min_passos) + self.dir_disco = dir_disco + self.manter_ckpt = int(manter_checkpoints) + self.hub = hub + self.total_mb = self._ram_total_mb() + self.limiar_mb = self.alpha * self.total_mb + self.n_limpezas = 0 + self.n_observacoes = 0 + self.ultimo_rss = rss_mb() + self.pico_rss = rss_mb() + self.ultimo_evento: dict = {} + + @staticmethod + def _ram_total_mb() -> float: + try: + with open("/proc/meminfo", "r") as f: + for linha in f: + if linha.startswith("MemTotal:"): + return float(linha.split()[1]) / 1024.0 + except OSError: + pass + return 4096.0 + + # ---------------- auto-ajuste do limiar (item j) ---------------- + def _ajustar_limiar(self): + """Histerese de Robbins–Monro: limpezas FREQUENTES (≥ 1 a cada + intervalo) ⇒ limiar desce 20% (intervenção cedo); raras ⇒ sobe 10% + (menos intervenção). Σ dos passos finito ⇒ converte.""" + if self.n_observacoes == 0: + return + taxa = self.n_limpezas / max(1, self.n_observacoes) + if taxa > 0.5: + self.alpha = max(0.55, self.alpha * 0.8) + elif taxa < 0.05: + self.alpha = min(0.90, self.alpha * 1.1) + self.limiar_mb = self.alpha * self.total_mb + + # ---------------- cascata de limpeza RAM ---------------- + def limpar_ram(self, motivo: str = "rotina") -> dict: + """Cascata: gc → torch caches → buffers → telemetria. Devolve o + RSS antes/depois (prova prática do Teorema 19.17).""" + antes = rss_mb() + coletadas = 0 + # 1) objetos Python ciclicos + coletadas = gc.collect() + # 2) caches internos do torch (CPU: fragmentação do alocador) + try: + if torch.cuda.is_available(): + torch.cuda.empty_cache() + except Exception: + pass + # 3) buffers anulares das GRUs (reboot dos micro buffers — item a): + # o buffer é pré-alocado — nada a liberar; apenas reinicia ponteiros + # via garbage collection de módulos não referenciados (gc já cobre) + # 4) histórico do hub (mantém EMA; corta séries longas) + if self.hub is not None: + try: + if hasattr(self.hub, "historico"): + for k in list(getattr(self.hub, "historico").keys()): + self.hub.historico[k] = self.hub.historico[k][-64:] + except Exception: + pass + depois = rss_mb() + self.n_limpezas += 1 + self.ultimo_evento = {"motivo": motivo, "gc": coletadas, + "rss_antes_mb": round(antes, 1), + "rss_depois_mb": round(depois, 1), + "liberado_mb": round(antes - depois, 1), + "ts": time.time()} + return self.ultimo_evento + + # ---------------- higiene de DISCO (armazenamento externo) ---------------- + def limpar_disco(self, dir_checkpoints: str | None = None) -> dict: + rel = {"arquivos_removidos": 0, "bytes_liberados": 0} + alvos = [] + if dir_checkpoints and os.path.isdir(dir_checkpoints): + ckpts = [] + for nome in os.listdir(dir_checkpoints): + caminho = os.path.join(dir_checkpoints, nome) + if os.path.isdir(caminho): + try: + mt = os.path.getmtime(os.path.join(caminho, "meta.json")) + except OSError: + mt = os.path.getmtime(caminho) + ckpts.append((mt, caminho)) + ckpts.sort(reverse=True) + for _, caminho in ckpts[self.manter_ckpt:]: + alvos.append(caminho) + if self.dir_disco and os.path.isdir(self.dir_disco): + for raiz, dirs, arqs in os.walk(self.dir_disco): + for d in dirs: + if d == "__pycache__": + alvos.append(os.path.join(raiz, d)) + for caminho in alvos: + try: + tam = sum(f.stat().st_size for f in Path(caminho).rglob("*") + if f.is_file()) if os.path.isdir(caminho) \ + else os.path.getsize(caminho) + shutil.rmtree(caminho) if os.path.isdir(caminho) \ + else os.remove(caminho) + rel["arquivos_removidos"] += 1 + rel["bytes_liberados"] += tam + except OSError: + continue + rel["bytes_liberados_mb"] = round(rel["bytes_liberados"] / 1e6, 1) + return rel + + # ---------------- ponto de verificação por passo ---------------- + def observar(self, passo: int, forcar: bool = False) -> dict | None: + """Chame a cada passo de treino: monitora RSS e dispara a cascata.""" + self.n_observacoes += 1 + self.ultimo_rss = rss_mb() + self.pico_rss = max(self.pico_rss, self.ultimo_rss) + if forcar or (self.ultimo_rss > self.limiar_mb + and self.n_observacoes % self.intervalo == 0): + ev = self.limpar_ram(motivo=f"passo {passo}: RSS {self.ultimo_rss:.0f} MB") + self._ajustar_limiar() + if self.hub is not None: + try: + self.hub.atributo("mem/rss_antes_mb", ev["rss_antes_mb"], passo) + self.hub.atributo("mem/liberado_mb", ev["liberado_mb"], passo) + except Exception: + pass + return ev + return None + + def estatisticas(self) -> dict: + return {"rss_mb": round(self.ultimo_rss, 1), + "pico_rss_mb": round(self.pico_rss, 1), + "limiar_mb": round(self.limiar_mb, 1), + "alpha": round(self.alpha, 3), + "limpezas": self.n_limpezas, + "observacoes": self.n_observacoes} diff --git a/src/khtst/telemetria/hub.py b/src/khtst/telemetria/hub.py new file mode 100644 index 0000000000000000000000000000000000000000..2fa48b094287f11bb4dd90b8c8388877db36b44a --- /dev/null +++ b/src/khtst/telemetria/hub.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +"""Telemetria (item 5): coleta de métricas, indicadores e evolução de TODOS os +parâmetros. Fundamentos matemáticos em docs/matematica/09 (§4) e 00 (§3). + +Escrita incremental em JSONL (baixo uso de RAM) + resumo em memória com EMAs. +""" +from __future__ import annotations + +import json +import math +import os +import time + +import numpy as np +import torch + + +def ema(anterior: float | None, valor: float, beta: float = 0.98) -> float: + return valor if anterior is None else beta * anterior + (1 - beta) * valor + + +def rank_efetivo(matriz: torch.Tensor, eps: float = 1e-12) -> float: + """Rank efetivo = exp(H(lambda_i normalizados)), Roy & Vetterli (2007).""" + with torch.no_grad(): + s = torch.linalg.svdvals(matriz.float()) + p = (s ** 2) / (torch.sum(s ** 2) + eps) + p = p[p > eps] + if p.numel() < 2: + return 1.0 + h = -(p * torch.log(p)).sum().item() + return math.exp(h) + + +def psi(baseline: np.ndarray, atual: np.ndarray, n_faixas: int = 10, eps: float = 1e-6) -> float: + """Population Stability Index entre distribuições (drift de ativações).""" + bordas = np.linspace(min(baseline.min(), atual.min()), + max(baseline.max(), atual.max()), n_faixas + 1) + p = np.histogram(baseline, bordas)[0] / max(len(baseline), 1) + eps + q = np.histogram(atual, bordas)[0] / max(len(atual), 1) + eps + return float(np.sum((p - q) * np.log(p / q))) + + +class TelemetryHub: + """Registro central: cada métrica vira linha JSONL + EMA em memória. + + Uso típico no loop de treino: + hub.passos(1, perda=2.31, grad_norm=0.9, lr=1e-3) + hub.atributo("som/gsom_eq", 0.42) + hub.parametros(modelo) # delta completo de todos os parâmetros (item 5) + """ + + def __init__(self, arquivo_jsonl: str, beta_ema: float = 0.98): + self.arquivo = arquivo_jsonl + self.beta = beta_ema + os.makedirs(os.path.dirname(arquivo_jsonl), exist_ok=True) + self._ema: dict[str, float] = {} + self._historico: dict[str, list[float]] = {} + self._anterior_theta: dict[str, torch.Tensor] | None = None + self.eventos_guard: list[str] = [] + self._inicio = time.time() + + # ---------------- coleta ---------------- + def passos(self, passo: int, **metricas: float) -> None: + linha = {"t": round(time.time() - self._inicio, 2), "passo": passo, **{ + k: round(float(v), 6) for k, v in metricas.items()}} + for k, v in metricas.items(): + self._ema[k] = ema(self._ema.get(k), float(v), self.beta) + self._historico.setdefault(k, []).append(float(v)) + self._gravar(linha) + + def atributo(self, nome: str, valor: float, passo: int | None = None) -> None: + linha = {"t": round(time.time() - self._inicio, 2), "passo": passo, + "atributo": nome, "valor": round(float(valor), 6)} + self._ema[nome] = ema(self._ema.get(nome), float(valor), self.beta) + self._historico.setdefault(nome, []).append(float(valor)) + self._gravar(linha) + + def parametros(self, modelo: torch.nn.Module, passo: int | None = None) -> float: + """Evolução completa de todos os parâmetros (item 5): delta por camada + global.""" + total_delta, total_norma, por_camada = 0.0, 0.0, {} + with torch.no_grad(): + for nome, p in modelo.named_parameters(): + delta, norma = 0.0, float(p.norm()) + if self._anterior_theta is not None and nome in self._anterior_theta: + d = p.detach().flatten() - self._anterior_theta[nome] + delta = float(torch.norm(d)) / (norma + 1e-12) + # guarda referência em CPU (RAM otimizada: fp32 → se >2GB usar fp16) + self._anterior_theta = self._anterior_theta or {} + # v3.1: amostra ≤ 24 camadas (RAM) — delta_global usa todas + if len(self._anterior_theta) < 24 or nome in self._anterior_theta: + self._anterior_theta[nome] = p.detach().flatten().cpu().clone() + total_delta += delta ** 2 + total_norma += norma ** 2 + por_camada[nome] = round(delta, 6) + global_delta = math.sqrt(total_delta) + linha = {"t": round(time.time() - self._inicio, 2), "passo": passo, + "atributo": "params/delta_global", "valor": round(global_delta, 8), + "por_camada": por_camada, "norma_global": round(math.sqrt(total_norma), 4)} + self._gravar(linha) + self._ema["params/delta_global"] = ema(self._ema.get("params/delta_global"), global_delta, self.beta) + return global_delta + + # ---------------- consulta ---------------- + def ema_de(self, nome: str) -> float | None: + return self._ema.get(nome) + + def janela(self, nome: str, n: int = 50) -> list[float]: + return self._historico.get(nome, [])[-n:] + + def resumo(self) -> dict: + return {"emas": {k: round(v, 6) for k, v in self._ema.items()}, + "n_pontos": {k: len(v) for k, v in self._historico.items()}, + "duracao_s": round(time.time() - self._inicio, 1)} + + def _gravar(self, linha: dict) -> None: + with open(self.arquivo, "a", encoding="utf-8") as f: + f.write(json.dumps(linha, ensure_ascii=False) + "\n") diff --git a/src/khtst/telemetria/indicadores.py b/src/khtst/telemetria/indicadores.py new file mode 100644 index 0000000000000000000000000000000000000000..51211fcade7c6d4503e2602005569c2c36de1ea1 --- /dev/null +++ b/src/khtst/telemetria/indicadores.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +"""Indicadores de saúde do treino (itens 5 e 6): estagnação (eq. 9.3, doc 09), +faixa de gradiente, PSI de drift e recomendação de punição/retreino. + +Separação responsabilidade: `hub` coleta; `indicadores` decide; `punicao` executa. +""" +from __future__ import annotations + +import numpy as np + +GRAD_MIN, GRAD_MAX = 1e-7, 1e2 + + +def estagnado(janela_perda: list[float], epsilon: float = 0.01) -> bool: + """Eq. (9.3): melhora relativa entre as duas metades da janela < epsilon.""" + if len(janela_perda) < 8: + return False + metade = len(janela_perda) // 2 + antigo = min(janela_perda[:metade]) + recente = min(janela_perda[metade:]) + return (antigo - recente) / max(antigo, 1e-12) < epsilon + + +def gradiente_fora_da_faixa(grad_norm: float) -> bool: + return not (GRAD_MIN <= grad_norm <= GRAD_MAX) + + +def avaliar(hub, cfg, passo: int, grad_norm: float | None = None) -> dict: + """Consolida indicadores do treino e recomenda ação (item 6).""" + perdas = hub.janela("perda/treino", cfg.treino.janela_estagnacao * 2) + estag = estagnado(perdas, cfg.treino.epsilon_estagnacao) + grad_ruim = gradiente_fora_da_faixa(grad_norm) if grad_norm is not None else False + drift = hub.ema_de("drift/psi_ativacoes") + rank = hub.ema_de("rep/rank_efetivo") + + sinais = { + "estagnacao": estag, + "gradiente_fora_faixa": grad_ruim, + "psi_alto": (drift is not None and drift > cfg.telemetria.psi_limite), + "rank_baixo": (rank is not None and rank < cfg.telemetria.rank_efectivo_min), + } + # política em ordem de agressividade (doc 09 §3) + if all(sinais.values()): + acao = "reinit_parcial_replay" + elif sinais["rank_baixo"] or sinais["gradiente_fora_faixa"]: + acao = "reinit_parcial" + elif sinais["estagnacao"] or sinais["psi_alto"]: + acao = "warm_restart" + elif any(sinais.values()): + acao = "observar" + else: + acao = "nenhuma" + return {"passo": passo, "sinais": sinais, "acao_recomendada": acao, + "ema_perda": hub.ema_de("perda/treino"), + "ema_psi": drift, "ema_rank": rank} + + +def resumo_janela(valores: list[float]) -> dict: + """Estatísticas descritivas para o relatório final (item 5).""" + if not valores: + return {} + a = np.asarray(valores, dtype=np.float64) + return {"n": int(a.size), "min": float(a.min()), "max": float(a.max()), + "media": float(a.mean()), "mediana": float(np.median(a)), + "dp": float(a.std()), "primeiro": float(a[0]), "ultimo": float(a[-1]), + "melhoria_%": float((a[0] - a[-1]) / max(abs(a[0]), 1e-12) * 100)} diff --git a/src/khtst/treino/__init__.py b/src/khtst/treino/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/khtst/treino/cirurgia_grad.py b/src/khtst/treino/cirurgia_grad.py new file mode 100644 index 0000000000000000000000000000000000000000..5683f86070d361fb14dd9157be3f7769601ad408 --- /dev/null +++ b/src/khtst/treino/cirurgia_grad.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +"""Cirurgia de gradiente PCGrad de duas perdas (doc 10 §5) — extraído do estudo +de BiGRU_T/gradient_surgery.py com correções: + +• `set_to_none=True` antes de atribuir (evita acúmulo com grads residuais); +• guarda ‖g_main‖² > eps também para o CASO DE PROJEÇÃO (não só para pular); +• perdas auxiliares são AGREGADAS antes (g_aux = ∇Σ aux) — 2 autograd.grad, + não N (custo CPU constante). + +Contrato: quem chama faz `optimizer.zero_grad()` antes e `optimizer.step()` +depois — aqui só se escreve `p.grad`. +""" +from __future__ import annotations + +import torch + + +def pcgrad_duas_perdas(modelo_ou_params, perda_main: torch.Tensor, + perda_aux: torch.Tensor, eps: float = 1e-8) -> dict: + """Calcula g_main e g_aux separadamente, projeta g_aux se ⟨g_main,g_aux⟩<0 + e escreve p.grad = g_main + g_aux'. Aceita um nn.Module ou lista de + parâmetros. Devolve telemetria.""" + if isinstance(modelo_ou_params, (list, tuple)): + params = list(modelo_ou_params) + else: + params = [p for p in modelo_ou_params.parameters() if p.requires_grad] + g_main = torch.autograd.grad(perda_main, params, retain_graph=True, + allow_unused=True) + g_aux = torch.autograd.grad(perda_aux, params, retain_graph=True, + allow_unused=True) + # produto interno global (sobre todos os parâmetros) + dot = 0.0 + n2_main = 0.0 + for gm, ga in zip(g_main, g_aux): + if gm is None or ga is None: + continue + dot += float((gm * ga).sum()) + n2_main += float((gm * gm).sum()) + projetado = dot < 0.0 and n2_main > eps + with torch.no_grad(): + fator = -dot / n2_main if projetado else 0.0 + for p, gm, ga in zip(params, g_main, g_aux): + if gm is None and ga is None: + continue + if gm is None: + g = ga + elif ga is None: + g = gm + else: + g = gm + (fator * ga if projetado else ga) + p.grad = g.detach() if p.grad is None else p.grad + g.detach() + return {"pcgrad/dot": dot, "pcgrad/projetado": float(projetado), + "pcgrad/fator": fator} diff --git a/src/khtst/treino/confianca.py b/src/khtst/treino/confianca.py new file mode 100644 index 0000000000000000000000000000000000000000..058dc33d6418edb695ba376130535609633211fd --- /dev/null +++ b/src/khtst/treino/confianca.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +"""AgenteConfiança (v3, doc 11 §8): confiança h_t ∈ [0,1] com garantias +finitas — posterior Beta(α,β) sobre θ*"o modelo está certo" com cotas de +Bernstein (Teorema 11.8) e PAC‑Bayes (Teorema 11.9 do ciclo/PDCA), fundida +com features neurais (entropia dos logits, max‑prob, variância, Δperda) e +pesos assimétricos κ_p > κ_r (punição pesa mais que prêmio — TAPR). + +Saídas consumidas por: + • ComposicaoMicroUnidades.registrar_confianca(h_t) — temperatura do gating + (menor confiança ⇒ softmax mais plano ⇒ mais exploração entre ramos); + • MTP adaptativo (doc 11 Teorema 11.6): máscara por posição via confiança + posicional 1 − H(p_t)/log V (monótona, calibrável); + • MemoriaInterna.escrever(·, h_t) — escrita conficiente; + • CicloPDCA — limiar bayesiano recebe a cota PAC‑Bayes. +""" +from __future__ import annotations + +import math +from collections import deque + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class PosteriorBeta: + """Beta‑Bernoulli com atualização por batch e cotas não-assintóticas.""" + + def __init__(self, alpha0: float = 1.0, beta0: float = 1.0): + self.a, self.b = alpha0, beta0 + self.a0, self.b0 = alpha0, beta0 + self.n = 0 + + def atualizar(self, acertos: int, erros: int): + self.a += acertos + self.b += erros + self.n += acertos + erros + + @property + def media(self) -> float: + return self.a / (self.a + self.b) + + @property + def variancia(self) -> float: + s = self.a + self.b + return self.a * self.b / (s * s * (s + 1.0)) + + def bernstein(self, delta: float = 0.05) -> tuple[float, float]: + """|μ̂ − θ*| ≤ √(2σ²ln(2/δ)) + (2/3)ln(2/δ)/(α+β) com prob ≥ 1−δ.""" + log_t = math.log(2.0 / delta) + eps = math.sqrt(2.0 * self.variancia * log_t) \ + + (2.0 / 3.0) * log_t / max(self.a + self.b, 1e-9) + mu = self.media + return max(0.0, mu - eps), min(1.0, mu + eps) + + def pac_bayes(self, perda_emp: float, n: int, delta: float = 0.05, + kl_prior: float = 0.0) -> float: + """E_post[L] ≤ L_emp + √((KL + ln(2√n/δ))/(2n)) — cota do risco do gate.""" + n = max(int(n), 1) + log_t = math.log(2.0 * math.sqrt(n) / delta) + return perda_emp + math.sqrt((kl_prior + log_t) / (2.0 * n)) + + +class AgenteConfianca(nn.Module): + """h_t = clip(½·μ̂_Beta + ½·h_neural, 0, 1) com κ assimétrico.""" + + def __init__(self, n_features: int = 4, oculto: int = 32, + kappa_punicao: float = 0.3, kappa_premio: float = 0.1, + janela_tau: int = 50, quantil_tau: float = 0.25): + super().__init__() + self.mlp = nn.Sequential(nn.Linear(n_features, oculto), nn.SiLU(), + nn.Linear(oculto, 1)) + self.kappa_p = kappa_punicao + self.kappa_r = kappa_premio + self.posterior = PosteriorBeta() + self.janela_perdas: deque = deque(maxlen=janela_tau) + self.quantil_tau = quantil_tau + self.tau: float | None = None + self.ultima_perda: float | None = None + self.calibracao: dict[int, list[int]] = {} # faixa h → [acertos, total] + self.ultimo_h: float = 0.5 + + # ---------------- τ adaptativo (quantil EMA) ---------------- + def _atualizar_tau(self, perda: float) -> float: + self.janela_perdas.append(perda) + janela = sorted(self.janela_perdas) + pos = self.quantil_tau * (len(janela) - 1) + lo = int(pos) + hi = min(lo + 1, len(janela) - 1) + q = janela[lo] * (1 - (pos - lo)) + janela[hi] * (pos - lo) + self.tau = q if self.tau is None else max(1e-6, 0.95 * self.tau + 0.05 * q) + return self.tau + + # ---------------- features e h ---------------- + @staticmethod + def features_logits(logits: torch.Tensor) -> torch.Tensor: + """logits (B,T,V) → features médias (4,): entropia, max‑prob, variância.""" + with torch.no_grad(): + p = torch.softmax(logits.float(), dim=-1) + H = -(p * (p + 1e-9).log()).sum(-1).mean() + mp = p.max(-1).values.mean() + var = logits.float().var() + return torch.tensor([float(H), float(mp), float(var), 0.0]) + + def forward(self, feats: torch.Tensor, perda_atual: float | None = None) -> torch.Tensor: + """feats (4,) → h_t escalar (tensor).""" + if perda_atual is not None: + delta = 0.0 if self.ultima_perda is None \ + else perda_atual - self.ultima_perda + feats = feats.clone() + feats[3] = max(min(delta, 5.0), -5.0) + self.ultima_perda = perda_atual + self._atualizar_tau(perda_atual) + h_raw = torch.sigmoid(self.mlp(feats.unsqueeze(0)))[0, 0] + # ajuste assimétrico: Δperda > 0 (pior) pune com κ_p; < 0 premia com κ_r + delta = float(feats[3]) + if delta > 0: + ajuste = -self.kappa_p * delta * (1.0 + abs(delta)) + else: + ajuste = self.kappa_r * abs(delta) + h_neural = torch.clamp(h_raw + ajuste, 0.0, 1.0) + mu = self.posterior.media + h = float(torch.clamp(0.5 * mu + 0.5 * h_neural.detach(), 0.0, 1.0)) + self.ultimo_h = h + return torch.tensor(h) + + # ---------------- acerto/erro (posterior + calibração) ---------------- + def registrar_resultado(self, perda: float, h: float | None = None): + """hit = perda < τ (quantil adaptativo). Atualiza posterior e ECE.""" + if self.tau is None: + return + hit = perda < self.tau + self.posterior.atualizar(int(hit), int(not hit)) + h = self.ultimo_h if h is None else h + faixa = min(int(h * 10), 9) + caixa = self.calibracao.setdefault(faixa, [0, 0]) + caixa[0] += int(hit) + caixa[1] += 1 + return hit + + def ece(self) -> float: + """Erro de calibração esperado (10 faixas) — monotonicidade do Teo 11.6.""" + n = sum(t for _, t in self.calibracao.values()) + if n == 0: + return 0.0 + e = 0.0 + for faixa, (acertos, total) in self.calibracao.items(): + conf_media = (faixa + 0.5) / 10.0 + e += (total / n) * abs(acertos / total - conf_media) + return e + + # ---------------- MTP adaptativo (Teorema 11.6) ---------------- + @staticmethod + def confianca_posicional(logits: torch.Tensor) -> torch.Tensor: + """logits (B,T,V) → h_pos (B,T) = 1 − H(p_t)/log V ∈ [0,1] — proxy + monotônico de confiança por posição (calibrável pelo ECE global).""" + with torch.no_grad(): + p = torch.softmax(logits.float(), dim=-1) + H = -(p * (p + 1e-9).log()).sum(-1) + return (1.0 - H / math.log(p.shape[-1])).clamp(0.0, 1.0) + + +def mascara_k_adaptativa(h_pos: torch.Tensor, k_min: int, k_max: int) -> torch.Tensor: + """h_pos (B,T) → máscara (B,T,K_max) com 1[k < K_t], + K_t = clamp(round(K_min + (K_max−K_min)·h), K_min, K_max).""" + k_cont = k_min + (k_max - k_min) * h_pos + k_t = torch.round(k_cont).long().clamp(k_min, k_max) + ks = torch.arange(k_max, device=h_pos.device).view(1, 1, k_max) + return (ks < k_t.unsqueeze(-1)).float() diff --git a/src/khtst/treino/dpo.py b/src/khtst/treino/dpo.py new file mode 100644 index 0000000000000000000000000000000000000000..03064bbf17473f3286fae3706ccc8b17b0287e99 --- /dev/null +++ b/src/khtst/treino/dpo.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +"""DPO (Direct Preference Optimization) com β adaptativo (doc 10 §4). + +Extraído do estudo de BiGRU_T/dpo.py com correções: +• sinal da dualidade corrigido: β DIMINUI quando a KL excede o alvo + (eq. 10.6: β_{t+1} = β_t·exp(−η(KL̄ − K))) — solta a âncora de referência; + o original documentava confusão de sinal. +• `pad_token_id` não é parâmetro morto: a máscara usa o ignore_index. +• Sem duplicação: uma única implementação da perda (o original tinha duas). + +Uso no KHTST v2: pares sintetizados a partir do corpus — escolhido = saída +dourada; rejeitado = corrupção por aumento de tokens (dropout/embaralhamento). +O modelo de referência é um clone congelado do SFT (state_dict clonado antes). +""" +from __future__ import annotations + +import torch +import torch.nn.functional as F + + +def logps_sequencia(logits: torch.Tensor, ids: torch.Tensor, + inicio_resposta: int = 1) -> torch.Tensor: + """Soma dos log-probs na região de resposta: logits (B,T,V), ids (B,T). + Convenção KHTST: logits[t] prevê ids[t+1] ⇒ logp do token t+1 vem de t. + Devolve (B,) com a soma a partir de `inicio_resposta` (exclui o prompt).""" + logp = torch.log_softmax(logits[:, :-1].float(), dim=-1) + alvo = ids[:, 1:] + tok_logp = logp.gather(-1, alvo.unsqueeze(-1)).squeeze(-1) # (B, T-1) + mascara = torch.zeros_like(tok_logp) + mascara[:, max(inicio_resposta - 1, 0):] = 1.0 + return (tok_logp * mascara).sum(dim=1) + + +class PerdaDPO: + """L = −logσ(β·Δ) com Δ = (π_c − ref_c) − (π_r − ref_r); β adaptativo.""" + + def __init__(self, beta: float = 0.1, lr_beta: float = 1e-3, + kl_alvo: float = 0.2, beta_min: float = 0.01, + beta_max: float = 2.0): + self.log_beta = torch.log(torch.tensor(float(beta))) + self.lr_beta, self.kl_alvo = lr_beta, kl_alvo + self.beta_min, self.beta_max = beta_min, beta_max + self.ultimo_kl: float | None = None + + @property + def beta(self) -> float: + return float(torch.exp(self.log_beta).clamp(self.beta_min, self.beta_max)) + + def __call__(self, pi_c: torch.Tensor, ref_c: torch.Tensor, + pi_r: torch.Tensor, ref_r: torch.Tensor) -> tuple[torch.Tensor, dict]: + delta = (pi_c - ref_c) - (pi_r - ref_r) + perda = -F.logsigmoid(self.beta * delta).mean() + with torch.no_grad(): + kl = (pi_c - ref_c).mean().abs() # proxy da KL por política + self.ultimo_kl = float(kl) + acc = float((delta > 0).float().mean()) + margem = float(delta.mean()) + return perda, {"dpo/acc": acc, "dpo/margem": margem, + "dpo/beta": self.beta, "dpo/kl": self.ultimo_kl} + + def atualizar_beta(self): + """Dual update (eq. 10.6): β_{t+1} = β_t·exp(−η(KL̄−K_alvo)) — β cai + quando KL excede o alvo (solta a âncora de referência).""" + if self.ultimo_kl is None: + return + self.log_beta = self.log_beta - self.lr_beta * (self.ultimo_kl - self.kl_alvo) + self.log_beta = self.log_beta.clamp(torch.log(torch.tensor(self.beta_min)), + torch.log(torch.tensor(self.beta_max))) diff --git a/src/khtst/treino/duas_fases.py b/src/khtst/treino/duas_fases.py new file mode 100644 index 0000000000000000000000000000000000000000..59e68e8495420fcf4f1781a19ecb9c82004db023 --- /dev/null +++ b/src/khtst/treino/duas_fases.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +"""Treino em DUAS FASES (doc 15 §2) — v4. + +Fase A (rede aumentada): tronco + MoE + MTP + NLP/NLG + DPO + punição/retreino. +Alinhamento SOM desligado (λ_som=0) — sem termo não-estacionário (Corolário 15.3). + +Fase B (consolidação SOM): tronco com lr ×0.1, os 8 SOMs + orquestrador +consolidam com o objetivo de MÁXIMO de neurônios ativos (eq. 15.1): + L_B = 0.3·L_CE + λ_ativos·(1−A)² + λ_nov·E[EQ incremental] +Teorema 15.2: reseeding guloso ⇒ A ≥ 1−(1−π_min)^J; alvo A ≥ 0.90. + +Uso: TreinadorDuasFases(fase='A'|'B') alterna o regime do treinador v3. +""" +from __future__ import annotations + +import torch + + +class RegimeDuasFases: + """Controla o regime de perdas/lr entre as fases A e B.""" + + def __init__(self, cfg): + self.cfg = cfg + self.fase: str = "A" + fs = dict(getattr(cfg.treino, "fase_som", {}) or {}) + self.alvo_ativos = float(fs.get("alvo_ativos", 0.90)) + self.lambda_ativos = 0.10 + self.lambda_nov = float(fs.get("lambda_novidade", 0.25)) + self.fator_lr_b = 0.1 + + def entrar_fase_a(self, modelo, otimizador): + self.fase = "A" + modelo.definir_fase_moe("densa") + for g in otimizador.param_groups: + g["fator_fase"] = 1.0 + return {"fase": "A", "lambda_som": 0.0, + "aviso": "alinhamento SOM desligado (Corolário 15.3)"} + + def entrar_fase_b(self, modelo, otimizador): + """Fase B: tronco reduzido, MoE em foco, SOM consolidando (eq. 15.1).""" + self.fase = "B" + modelo.definir_fase_moe("foco") + for g in otimizador.param_groups: + g["fator_fase"] = self.fator_lr_b + return {"fase": "B", "lambda_som": 0.10, + "alvo_ativos": self.alvo_ativos} + + # ---------------- perda da fase B (eq. 15.1) ---------------- + def perda_fase_b(self, modelo, orquestrador, ids: torch.Tensor, + perda_ce: torch.Tensor) -> tuple[torch.Tensor, dict]: + """L_B = 0.3·CE + λ_ativos·(1−A)² + λ_nov·EQ_médio.""" + A = orquestrador.taxa_ativos_global() if orquestrador is not None else 1.0 + perda_ativos = (1.0 - A) ** 2 + eq_medio = 0.0 + if orquestrador is not None: + ctx = modelo.contexto(ids).detach() + eq_medio = orquestrador.eq_medio(ctx) + total = (0.3 * perda_ce + + self.lambda_ativos * perda_ativos + + self.lambda_nov * eq_medio) + info = {"fase": "B", "A_ativos": float(A), + "perda_ativos": float(perda_ativos), "eq_medio": float(eq_medio), + "alvo_ativos": self.alvo_ativos} + return total, info + + def proxima_fase(self, epoca_atual: int, epocas_fase_a: int) -> str: + return "B" if epoca_atual >= epocas_fase_a else "A" diff --git a/src/khtst/treino/mtp.py b/src/khtst/treino/mtp.py new file mode 100644 index 0000000000000000000000000000000000000000..b72fdd9e4759e965af92eab3cb9a25fb523927b7 --- /dev/null +++ b/src/khtst/treino/mtp.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +"""Predição multi-token (MTP) com pesos α aprendíveis (doc 10 §3) — item extraído +do estudo de BiGRU_T/mtp.py e CNN-BiGRU/medusa_heads.py, com correções: + +• Custo K·d em vez de K·V·d: os logits usam a tabela de embeddings EMPATADA + (logits^{(m)} = E · SiLU(W_m h)) — o Medusa do CNN-BiGRU custava 4·32000·512 + ≈ 65,6M de parâmetros; aqui custa K·d². +• α via softmax com regularizador de entropia (Teorema 10.8: razão α_m/α_n + limitada por exp(−ΔL/β) — anti-colapso). +• Alinhamento correto por deslocamento: cabeça m prediz alvo_{m:T} nas posições + 0:T−m (os proxies inválidos tipo logits[:, :, :D] do nlg.py NÃO ocorrem aqui: + a entrada é o estado oculto real). +""" +from __future__ import annotations + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class MTPKHTST(nn.Module): + def __init__(self, d: int, k_cabecas: int = 2, beta_entropia: float = 0.01): + super().__init__() + assert k_cabecas >= 1 + self.k = k_cabecas + self.beta = beta_entropia + self.w = nn.ModuleList([nn.Linear(d, d, bias=False) for _ in range(k_cabecas)]) + self.log_alphas = nn.Parameter(torch.zeros(k_cabecas)) # softmax → uniforme + self.ultimos: dict = {} + self._peso_emb: torch.Tensor | None = None # v4 (rascunho) + + def alphas(self) -> torch.Tensor: + return torch.softmax(self.log_alphas, dim=-1) + + def perda(self, h: torch.Tensor, peso_emb: torch.Tensor, alvo: torch.Tensor, + ignore_index: int = -100, + h_conf: torch.Tensor | None = None) -> torch.Tensor: + """h: (B,T,d) estado oculto real; peso_emb: (V,d) tabela empatada; + alvo: (B,T) rótulos LM com -100. + + v3 (doc 11 Teorema 11.6): se `h_conf` (B,T) ∈ [0,1] é fornecido, a + perda usa MÁSCARA DE K ADAPTATIVO — a posição t só contribui para as + cabeças m < K_t = clamp(round(K_min + (K_max−K_min)·h_t), K_min, K). + Economia esperada ≈ K_max·E[h] + K_min·(1−E[h]) termos de CE.""" + B, T, d = h.shape + alphas = self.alphas() + total = h.new_zeros(()) + per_cabeca = [] + soma_mask = 0.0 + for m in range(self.k): + desloc = m + 1 + if T - desloc <= 0: + per_cabeca.append(float("nan")) + continue + hm = F.silu(self.w[m](h[:, :T - desloc])) # (B, T-m, d) + logits = F.linear(hm, peso_emb) # (B, T-m, V) + tgt = alvo[:, desloc:] # (B, T-m) + if h_conf is not None: + # máscara K adaptativa por posição (K_t ≥ 1 ⇒ cabeça 0 sempre on) + k_min = 1 + k_cont = k_min + (self.k - k_min) * h_conf[:, :T - desloc] + k_t = torch.round(k_cont).long().clamp(k_min, self.k) + mask_m = (k_t > m).float() # (B, T-m) + ce_elem = F.cross_entropy( + logits.reshape(-1, logits.shape[-1]), tgt.reshape(-1), + ignore_index=ignore_index, reduction="none") + valid = (tgt.reshape(-1) != ignore_index).float() \ + * mask_m.reshape(-1) + soma = (ce_elem * valid).sum() / valid.sum().clamp(min=1.0) + soma_mask += float(valid.sum()) + else: + soma = F.cross_entropy(logits.reshape(-1, logits.shape[-1]), + tgt.reshape(-1), ignore_index=ignore_index) + per_cabeca.append(float(soma.detach())) + total = total + alphas[m] * soma + if all(isinstance(p, float) and p != p for p in per_cabeca): + return h.new_zeros(()) # sequências curtas + # regularizador de entropia (Teorema 10.8): maximiza H(α) + H = -(alphas * torch.log(alphas + 1e-8)).sum() + total = total - self.beta * H + self.ultimos = {"alphas": [float(a) for a in alphas.detach()], + "per_cabeca": per_cabeca, "entropia": float(H.detach()), + "termos_ce": soma_mask} + return total + + @torch.no_grad() + def logits_rascunho(self, h_ultimo: torch.Tensor) -> list[torch.Tensor]: + """v4 (doc 14): logits do RASCUNHO por cabeça para o decodificador + especulativo. h_ultimo: (B, d) ou (B, 1, d) → lista de K tensores (B, V) + EMPATADOS com a tabela do modelo (custo K·d², doc 10 §3).""" + if h_ultimo.dim() == 2: + h_ultimo = h_ultimo.unsqueeze(1) # (B, 1, d) + return [F.linear(F.silu(w(h_ultimo)), self._peso_emb) + for w in self.w] + + def definir_peso_emb(self, peso_emb: torch.Tensor): + """Guarda referência (não-parâmetro) da tabela empatada p/ rascunho.""" + self._peso_emb = peso_emb diff --git a/src/khtst/treino/perdas.py b/src/khtst/treino/perdas.py new file mode 100644 index 0000000000000000000000000000000000000000..88233ae1b92e425d62fe37ebb951ff69e64a1818 --- /dev/null +++ b/src/khtst/treino/perdas.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +"""Perdas multi-tarefa (item 6): CE principal + auxiliares com pesos +fundamentados (Teorema 6.2 p/ normalização de escalas de gradiente). +""" +from __future__ import annotations + +import torch + + +PESO_MOE = 0.02 # balanceamento cooperativo (eq. 9.2) — mínimo 1/N já baixo +PESO_ALINHAMENTO_SOM = 0.05 # alinhamento à memória (MSE) — termo fraco por Teorema 6.3 + + +def perda_total(perda_ce: torch.Tensor, perda_moe: float | torch.Tensor, + perda_som: torch.Tensor | None) -> torch.Tensor: + total = perda_ce + if perda_moe is not None: + try: + total = total + PESO_MOE * (perda_moe if isinstance(perda_moe, torch.Tensor) + else torch.tensor(perda_moe)) + except Exception: + pass + if perda_som is not None: + total = total + PESO_ALINHAMENTO_SOM * perda_som + return total diff --git a/src/khtst/treino/prs_v8.py b/src/khtst/treino/prs_v8.py new file mode 100644 index 0000000000000000000000000000000000000000..145acb38c826142b5f88ae0f2a061d1659ad9445 --- /dev/null +++ b/src/khtst/treino/prs_v8.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +"""PRS V8 — Sistema de Punição‑Recompensa com cinco mecanismos provados +(doc 11 §7, Teoremas 11.7a–11.7f), extraídos e corrigidos do estudo +`xavante_work/flexnet/adaptive_prs.py`: + + 7.1 Trust EMA com HISTERESE ASSIMÉTRICA (η_up=0.05, η_down=0.02): + E[T∞] = p·η_up / (p·η_up + (1−p)·η_down) — recuperação 2,5× mais + lenta que a perda ⇒ misses transitórios em platô não desabam o trust. + 7.2 LR SGDR com PISO DECRESCENTE entre ciclos (η_min^{(i)} = η_min·d^i): + energia de exploração não cresce ⇒ Robbins–Monro preservado. + 7.3 Grad‑clip por PERCENTIL ROBUSTO Q₀.₇₅ (breakdown point 0.25 vs 0 da + média±kσ — um gradiente explosivo não relaxa o clip). + 7.4 β_min/δ_min DOIS REGIMES (T>0.5: exploração; T≤0.5: força correção). + 7.5 Recompensa de sequência LOGARÍTMICA 1 + c·log(1+k) (variância finita + para k geométrico, sem saturação artificial). + 7.6 Boost de velocidade por SIGMOIDE suave (sem chattering no limiar). + +Defeito 5 corrigido: buffers de τ e de velocidade SEPARADOS (a versão +estudada media velocidade sobre a mesma janela que alimenta τ). +""" +from __future__ import annotations + +import math +from collections import deque + + +class PRSV8: + def __init__(self, cfg: dict | None = None): + c = dict(cfg or {}) + # 7.1 histerese + self.eta_up = float(c.get("eta_up", 0.05)) + self.eta_down = float(c.get("eta_down", 0.02)) + self.trust = 0.5 + # 7.2 SGDR + piso decrescente + self.warmup = int(c.get("warmup", 60)) + self.T_ciclo = int(c.get("T_ciclo", 400)) + self.lr_max = float(c.get("lr_max", 3e-3)) + self.lr_min0 = float(c.get("lr_min", 3e-4)) + self.decay_piso = float(c.get("decay_piso", 0.5)) + self.ciclos = 0 + # 7.3 clip percentil + self.janela_clip: deque = deque(maxlen=int(c.get("janela_clip", 50))) + self.clip_min = float(c.get("clip_min", 0.1)) + self.clip_base = float(c.get("clip_base", 1.0)) + self.percentil = float(c.get("percentil", 0.75)) + # 7.5/7.6 + self.streak = 0 + self.coef_streak = float(c.get("coef_streak", 0.3)) + self.vel_janela: deque = deque(maxlen=int(c.get("vel_janela", 20))) + self.vel_alvo = float(c.get("vel_alvo", 0.01)) + self.boost_max = float(c.get("boost_max", 1.5)) + self.alfa_sig = float(c.get("alfa_sig", 20.0)) + # τ adaptativo para hit/miss (janela PRÓPRIA — defect 5) + self.janela_tau: deque = deque(maxlen=int(c.get("janela_tau", 50))) + self.tau: float | None = None + # telemetria + self.recompensa_acum = 0.0 + self.clip_atual = self.clip_base + self.boost_atual = 1.0 + + # ---------------- 7.1 ---------------- + def atualizar_trust(self, hit: bool) -> float: + eta = self.eta_up if hit else self.eta_down + alvo = 1.0 if hit else 0.0 + self.trust = (1 - eta) * self.trust + eta * alvo + return self.trust + + def registrar_perda(self, perda: float): + """τ = quantil EMA da janela própria (defeito 5 corrigido).""" + self.janela_tau.append(perda) + j = sorted(self.janela_tau) + pos = 0.25 * (len(j) - 1) + lo = int(pos) + hi = min(lo + 1, len(j) - 1) + q = j[lo] * (1 - (pos - lo)) + j[hi] * (pos - lo) + self.tau = q if self.tau is None else max(1e-6, 0.95 * self.tau + 0.05 * q) + return self.tau + + def hit(self, perda: float) -> bool: + return self.tau is not None and perda < self.tau + + def passo_batch(self, perda: float) -> bool: + """Registra perda, decide hit, atualiza trust e streak. → hit""" + self.registrar_perda(perda) + h = self.hit(perda) + self.atualizar_trust(h) + # 7.5 recompensa de sequência + self.streak = self.streak + 1 if h else 0 + base = max(0.0, (self.tau - perda)) if self.tau else 0.0 + recompensa = base * (1.0 + self.coef_streak * math.log1p(self.streak)) + self.recompensa_acum += recompensa + return h + + # ---------------- 7.2 + 7.6 ---------------- + def registrar_velocidade(self, velocidade: float) -> float: + """velocidade > 0 = perda caindo. Boost suave por sigmoide (7.6).""" + self.vel_janela.append(velocidade) + if len(self.vel_janela) < 2: + self.boost_atual = 1.0 + return 1.0 + media = sum(self.vel_janela) / len(self.vel_janela) + sig = 1.0 / (1.0 + math.exp(-self.alfa_sig * (media - self.vel_alvo))) + self.boost_atual = 1.0 + 0.5 * (self.boost_max - 1.0) * sig + return self.boost_atual + + def lr(self, t: int) -> float: + """SGDR com piso decrescente + boost + trava por trust baixo.""" + if t < self.warmup: + eta = self.lr_max * (t + 1) / self.warmup + else: + s = (t - self.warmup) % max(1, self.T_ciclo) + idx = (t - self.warmup) // max(1, self.T_ciclo) + self.ciclos = max(self.ciclos, idx) + piso = self.lr_min0 * (self.decay_piso ** idx) + eta = piso + 0.5 * (self.lr_max - piso) * (1 + math.cos(math.pi * s / self.T_ciclo)) + eta = max(eta, 0.5 * piso) + eta *= self.boost_atual + if self.trust < 0.1: # trava de segurança (V7) + eta *= max(0.1, self.trust ** 2) + return eta + + # ---------------- 7.3 ---------------- + def clip(self, grad_norm: float) -> float: + """clip(t) = max(clip_min, Q₀.₇₅(janela)), cap 5·clip_base.""" + self.janela_clip.append(grad_norm) + if len(self.janela_clip) >= 5: + j = sorted(self.janela_clip) + pos = self.percentil * (len(j) - 1) + lo = int(pos) + hi = min(lo + 1, len(j) - 1) + c = j[lo] * (1 - (pos - lo)) + j[hi] * (pos - lo) + else: + c = grad_norm + self.clip_atual = min(max(c, self.clip_min), self.clip_base * 5.0) + return self.clip_atual + + # ---------------- 7.4 ---------------- + def beta_min(self, beta_min0: float = 0.01) -> float: + if self.trust > 0.5: + return beta_min0 * self.trust + return beta_min0 * 2.0 + + # ---------------- estado ---------------- + def estado(self) -> dict: + return {"trust": round(self.trust, 4), + "tau": round(self.tau, 5) if self.tau else None, + "clip": round(self.clip_atual, 4), + "boost": round(self.boost_atual, 3), + "streak": self.streak, + "ciclos_sgdr": self.ciclos, + "recompensa_acum": round(self.recompensa_acum, 3), + "beta_min_relativo": round(self.beta_min(1.0), 3)} diff --git a/src/khtst/treino/punicao.py b/src/khtst/treino/punicao.py new file mode 100644 index 0000000000000000000000000000000000000000..78a3ad34ac8e2d12b968702b99610f5fee71f1bb --- /dev/null +++ b/src/khtst/treino/punicao.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +"""Punição e retreino (item 6, doc 09 §3): detectar não-aprendizado pela +telemetria e reagir em ordem de agressividade — warm restart (SGDR), reinit +parcial guiado por contribuição de gradiente, replay do buffer. + +Teorema 9.4: punições finitas preservam Robbins–Monro ⇒ convergência intacta. +""" +from __future__ import annotations + +import torch +import torch.nn as nn + +from khtst.telemetria.indicadores import avaliar + + +class PoliticaRetreino: + def __init__(self, treinador, cfg, hub=None): + self.treinador = treinador + self.cfg = cfg + self.hub = hub + self.ultima_acao_no_passo = -10**9 + self.punicoes_epoca = 0 + # v6 (doc 18 Teorema 18.5): SGDR com T_mult — ciclo i tem T_i = T_0·m^i + self.ciclo_sgdr = 0 + self.T_0 = cfg.treino.janela_estagnacao + self.t_mult = max(1, int(getattr(cfg.treino, "sgdr_t_mult", 2))) + # v7 — eq. 9.3 CLÁUSULA 2: histórico da EMA da perda para detectar + # platos RUIDOSOS (o min por janela engana com oscilação de ±1%) + self._k_ema = 10 + self._ema_hist: list[float] = [] + self._estag_ema_consecutivos = 0 # escada de agressividade (doc 09 §3) + + def nova_epoca(self): + self.punicoes_epoca = 0 + + # ---------------- verificação ---------------- + def verificar(self, passo: int, grad_norm: float | None) -> str | None: + cfg = self.cfg.treino + # v7 — eq. 9.3 cláusula 2: EMA(ℓ) registrada a cada verificação + # (acumula MESMO em cooldown, p/ janela contínua) + ema = self.hub.ema_de("perda/treino") if self.hub is not None else None + if ema is not None and ema == ema: + self._ema_hist.append(float(ema)) + if len(self._ema_hist) > self._k_ema: + self._ema_hist.pop(0) + # v8.2 (KHTST) — cooldown = T_0 (antes: janela/2): um reinício por + # ciclo SGDR no máximo; reinícios em cadeia esmagam o lr no piso + # (16 reinícios/2075 passos medidos ⇒ 43% do tempo com lr < 0,001). + if passo - self.ultima_acao_no_passo < self.T_0: + return None # cooldown = ciclo completo + if self.punicoes_epoca >= cfg.max_punicoes_por_epoca: + return None + # v8.2 — GUARDA DE AQUECIMENTO também na cláusula 1 (bug v7: a + # cláusula 1 disparava no passo 9, antes de qualquer aprendizado) + if passo <= getattr(cfg, "warmup", 60) + self.T_0: + return None + diag = avaliar(self.hub, self.cfg, passo, grad_norm) + acao = diag["acao_recomendada"] + # v7 — cláusula 2 de (9.3): "EMA_t(ℓ) crescente por k janelas" — + # EMA sem melhora relativa ≥ ε em k verificações ⇒ estagnado + # (só depois do aquecimento; platos ruidosos não disparam a cláusula 1) + if acao in ("nenhuma", "observar") and len(self._ema_hist) >= self._k_ema \ + and passo > getattr(cfg, "warmup", 60) + self.T_0: + antigo, recente = self._ema_hist[0], self._ema_hist[-1] + if (antigo - recente) / max(abs(antigo), 1e-12) < cfg.epsilon_estagnacao: + # ordem de agressividade: 1º/2º warm_restart; persistindo o plato, + # reinit_parcial_replay (Teorema 9.2 — camada dorminhoca + replay) + self._estag_ema_consecutivos += 1 + acao = ("warm_restart" if self._estag_ema_consecutivos <= 2 + else "reinit_parcial_replay") + else: + self._estag_ema_consecutivos = 0 + if acao in ("nenhuma", "observar"): + return None + return acao + + # ---------------- execução ---------------- + def executar(self, acao: str, passo: int) -> dict: + self.ultima_acao_no_passo = passo + self.punicoes_epoca += 1 + resultado = {"acao": acao, "passo": passo} + if acao == "warm_restart": + # v6 — SGDR T_mult: T_i = T_0·m^i (recomeços FINITOS, Teorema 18.5) + T_i = self.T_0 * (self.t_mult ** min(self.ciclo_sgdr, 6)) + self.treinador.reiniciar_ciclo(T_i) + self.ciclo_sgdr += 1 + resultado["detalhe"] = f"SGDR T_mult: ciclo {self.ciclo_sgdr}, T={T_i}" + elif acao.startswith("reinit_parcial"): + alvo = self._camada_dorminhoca() + if alvo is not None: + self._reinit(alvo) + resultado["detalhe"] = f"camada reinicializada: {alvo[0]}" + if acao == "reinit_parcial_replay": + self.treinador.modo_replay = 3 + resultado["detalhe"] += " + replay 3 lotes" + if self.hub is not None: + self.hub.atributo("punicao/acao", hash(acao) % 1000, passo) + # v6 — RPP: punição registra no GestorRPP (higiene do canal REWARD) + rpp = getattr(self.treinador, "rpp", None) + if rpp is not None: + rpp.registrar_punicao(acao, passo) + return resultado + + # ---------------- utilidades ---------------- + def _camada_dorminhoca(self): + """Camada com menor contribuição c_l = E‖g_l‖·‖W_l‖ (Teorema 9.2).""" + contrib = self.treinador.contribuicao_grad + candidatas = [(nome, m) for nome, m in self.treinador.modelo.named_modules() + if isinstance(m, nn.Linear) and f"{nome}.weight" in contrib] + melhor = None + for nome, modulo in candidatas: + chave = f"{nome}.weight" + c = contrib.get(chave, 0.0) + if melhor is None or c < melhor[0]: + melhor = (c, nome, modulo) + return (melhor[1], melhor[2]) if melhor else None + + @staticmethod + def _reinit(item: tuple[str, nn.Linear]) -> None: + _, modulo = item + nn.init.orthogonal_(modulo.weight) + if modulo.bias is not None: + nn.init.zeros_(modulo.bias) diff --git a/src/khtst/treino/rpp.py b/src/khtst/treino/rpp.py new file mode 100644 index 0000000000000000000000000000000000000000..68c478e3ab1924d23e99e55c626163df86f1fbd4 --- /dev/null +++ b/src/khtst/treino/rpp.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +"""REWARD / PUNISHMENT / PENALTY (doc 18 §2) — controlador de 3 canais (v6). + + θ_{t+1} = θ_t − α_t·ρ_t·(∇L + Σ κ_k ∇Ω_k) (eq. 18.2) + + REWARD ρ_t ∈ [1−ρ̄, 1+ρ̄] — multiplicador de lr com GATE de integridade + (Proposição 18.7): só atua com melhora > 1σ da janela, SOM + saudável (taxa_ativos ≥ alvo) e streak ≥ 2; caso contrário ρ=1. + PUNISHMENT ações discretas: SGDR com T_mult (Teorema 18.5 — recomeços + finitos, mínimos preservados), reinit parcial (doc 09 §3), + replay. Delegado à PoliticaRetreino existente. + PENALTY termos aditivos LIMITADOS (κ finitos): novidade restrita a + empates de Voronoi (doc 10 §2.3a — Teorema 18.6), balanceamento + MoE, ortogonalidade intra-grupo (já no _passo) + penalidade de + ROTAS MORTAS (experts com uso_ema → 0) da v6. + +Teorema 18.4: com ρ_t limitado e κ finitos, Robbins–Monro permanece intacto. +""" +from __future__ import annotations + +import statistics + +import torch + + +class GestorRPP: + """Estado e decisões do controlador RPP (canal REWARD + PENALTY extra).""" + + def __init__(self, cfg: dict, hub=None): + # cfg: treino.rpp + self.rho = 1.0 # multiplicador corrente do lr + self.rho_max = float(cfg.get("rho_max", 0.15)) # ρ̄ + self.rho_step = float(cfg.get("rho_step", 0.05)) + self.rho_decaimento = float(cfg.get("rho_decaimento", 0.5)) + self.janela = int(cfg.get("janela", 30)) + self.kappa_rotas_mortas = float(cfg.get("kappa_rotas_mortas", 0.01)) + self.alvo_ativos = float(cfg.get("alvo_ativos", 0.90)) + self.hub = hub + self._perdas: list[float] = [] + self._streak = 0 + self.eventos: list[dict] = [] + self.recompensas = 0 + self.punicoes = 0 + self.penalidades = 0 + + # ---------------- REWARD ---------------- + def registrar_perda(self, perda: float) -> None: + self._perdas.append(perda) + if len(self._perdas) > self.janela: + self._perdas.pop(0) + if len(self._perdas) >= 8: + meta = self._perdas[: len(self._perdas) // 2] + atual = self._perdas[len(self._perdas) // 2:] + melhorou = min(atual) < min(meta) - statistics.pstdev(meta) + self._streak = self._streak + 1 if melhorou else 0 + + def decidir_reward(self, taxa_ativos_som: float | None) -> float: + """Proposição 18.7: gate de integridade do REWARD. Devolve ρ_t.""" + elegivel = (self._streak >= 2 + and len(self._perdas) >= 8 + and (taxa_ativos_som is None + or taxa_ativos_som >= self.alvo_ativos)) + if elegivel: + self.rho = min(1.0 + self.rho_max, self.rho + self.rho_step) + self.recompensas += 1 + self.eventos.append({"tipo": "reward", "rho": round(self.rho, 4), + "streak": self._streak}) + else: + self.rho = 1.0 + (self.rho - 1.0) * self.rho_decaimento + return self.rho + + def lr_efetivo(self, lr_base: float) -> float: + return lr_base * self.rho + + # ---------------- PENALTY (termo extra v6) ---------------- + def penalidade_rotas_mortas(self, modelo) -> torch.Tensor | None: + """PENALTY de rotas mortas: soma de relu(θ_morto − uso_ema_e) sobre os + experts — empurra o gate a reativar experts adormecidos (κ limitado). + Retorna None se não aplicável (custo zero no fluxo).""" + termos = [] + for bloco in modelo.blocos: + moe = getattr(bloco, "moe", None) + if moe is None: + continue + # v8 — MoE enc-dec: penaliza rotas mortas do DECODER (4 experts) + if getattr(moe, "_tipo_enc_dec", False): + if isinstance(getattr(moe, "uso_dec_ema", None), torch.Tensor): + termos.append(torch.relu(1.0 / moe.n_dec - moe.uso_dec_ema).sum()) + continue + if moe.uso_ema is not None: + limite = 1.0 / moe.N + termos.append(torch.relu(limite - moe.uso_ema).sum()) + if not termos: + return None + self.penalidades += 1 + return self.kappa_rotas_mortas * (termos[0] / len(termos)) + + # ---------------- PUNISHMENT (integração com a política existente) ---- + def registrar_punicao(self, acao: str, passo: int) -> None: + self.punicoes += 1 + self.eventos.append({"tipo": "punishment", "acao": acao, + "passo": passo}) + # punição reseta o streak e o reward (higiene do canal) + self._streak = 0 + self.rho = 1.0 + + def estado(self) -> dict: + return {"rho": round(self.rho, 4), "streak": self._streak, + "recompensas": self.recompensas, "punicoes": self.punicoes, + "penalidades": self.penalidades, + "kappa_rotas_mortas": self.kappa_rotas_mortas} diff --git a/src/khtst/treino/treinador.py b/src/khtst/treino/treinador.py new file mode 100644 index 0000000000000000000000000000000000000000..8eeed9d119dda48508a484bd54968109ea8228e2 --- /dev/null +++ b/src/khtst/treino/treinador.py @@ -0,0 +1,952 @@ +# -*- coding: utf-8 -*- +"""Treinador extensão v2 — pipeline completo do escopo (itens a–d): + +FASE 1 densa (rede aprimorada, MÁXIMO de conexões): todos os parâmetros + recebem gradiente; MoE com gate softmax sobre TODOS os experts; MTP + ativa; PCGrad entre CE e auxiliares (doc 10 §5); barreira ABMO no lr + (doc 10 §6); punição/retreino (item 6); checkpoints no HuggingFace + (item c — salvar E USAR estados) substituído por checkpoints LOCAIS com + higiene de disco; snapshot final entra no commit único de publicação + (doc 16 §8 — sem uploads parciais no Hub). +FASE 2 foco (última época): MoE top-k + máscara de tarefa (ignora irrelevante + por indexação) + QAT-W8A8 nos passos finais (item 9). +FASE 3 DPO: pares (dourado, corrompido) com β adaptativo (doc 10 §4). +FASE 4 SOM: consolidação com MÁXIMO de neurônios ativos (doc 10 §2.3): + novidade restrita a empates + reseeding + taxa_ativos ≥ alvo. + +RAM: lotes dinâmicos, replay limitado, snapshot de gradiente em CPU apenas. +""" +from __future__ import annotations + +import io +import math +import os +import random +import time +from collections import deque + +import torch + +from khtst.dados.checkpoints import GestorCheckpoints +from khtst.dados.streaming import _de_b64 +from khtst.memoria.interna import MemoriaInterna +from khtst.nucleo.modelo import KHTSTModel +from khtst.percepcao.microunidades import GestorCrescimento +from khtst.percepcao.ortogonais import CamadaOrtogonal +from khtst.telemetria.hub import TelemetryHub +from khtst.telemetria.indicadores import resumo_janela +from khtst.treino.cirurgia_grad import pcgrad_duas_perdas +from khtst.treino.confianca import AgenteConfianca +from khtst.treino.dpo import PerdaDPO, logps_sequencia +from khtst.treino.perdas import perda_total +from khtst.treino.prs_v8 import PRSV8 +from khtst.treino.punicao import PoliticaRetreino +from khtst.treino.rpp import GestorRPP +from khtst.percepcao.roteador_ssom import LISTA_TAREFAS + +TAREFAS_IMAGEM = ("vqa", "ocr", "imagem_caption") +TAREFAS_AUDIO = ("asr",) + + +def _imagem_para_tensor(dados_jpeg: bytes, lado: int = 96) -> torch.Tensor: + """JPEG bytes → tensor (3, lado, lado) em [0,1].""" + from PIL import Image + img = Image.open(io.BytesIO(dados_jpeg)).convert("RGB").resize((lado, lado)) + import numpy as np + arr = np.asarray(img, dtype=np.float32) / 255.0 # (H,W,3) + return torch.from_numpy(arr).permute(2, 0, 1) # (3,H,W) + + +def _audio_para_tensor(onda_fp16: bytes) -> torch.Tensor: + """bytes float16 16kHz → tensor (N,) float32.""" + import numpy as np + y = np.frombuffer(onda_fp16, dtype=" float: + cfg = self.cfg.treino + # v3 fix: o LR do PRS V8 (SGDR+boost) mostrou-se quente demais para este + # porte (reinícios a cada T_ciclo reinjetavam η_max e a perda subia entre + # épocas). O PRS V8 permanece para TRUST/CLIP/STREAK (Teoremas 7.1/7.3/ + # 7.5 — valores saudáveis na telemetria); o cronograma volta ao cosine + # warm-restart da v2 (provenido estável). + if self.t_ciclo < cfg.warmup: + return cfg.lr_max * (self.t_ciclo + 1) / cfg.warmup + p = (self.t_ciclo - cfg.warmup) / max(self.T_ciclo - cfg.warmup, 1) + p = min(p, 1.0) + return cfg.lr_min + 0.5 * (cfg.lr_max - cfg.lr_min) * (1 + math.cos(math.pi * p)) + + def lr(self) -> float: + """lr com barreira analítica: η_eff = min(lr, (2−m)/L̂) (Teorema 10.11).""" + lr = self.lr_base() + if not self._abmo.get("ativo", True): + return lr + cap = (2.0 - float(self._abmo.get("margem", 0.25))) / max(self._L_hat, 1e-6) + if cap < lr: + self.barreira_ativa_count += 1 + return min(lr, cap) + + def _atualizar_l_hat(self, perda: float): + if self._L_ref is None: + self._L_ref = max(abs(perda), 1e-6) + infl = abs(perda) / self._L_ref + cap_min = float(self._abmo.get("cap_min", 0.05)) + cap_max = float(self._abmo.get("cap_max", 500.0)) + self._L_hat = min(max(0.9 * self._L_hat + 0.1 * infl, cap_min), cap_max) + + # ---------------- lotes (multimodais reais) ---------------- + def _escolher_tarefa(self) -> str: + mistura = self.cfg.treino.mistura_tarefas + disponiveis = {k: v for k, v in mistura.items() if self.por_tarefa.get(k)} + if not disponiveis: + return next(iter(self.por_tarefa)) + soma = sum(disponiveis.values()) + r = random.random() * soma + acum = 0.0 + for k, v in disponiveis.items(): + acum += v + if r <= acum: + return k + return next(iter(disponiveis)) + + def _montar_lote(self, tarefa: str, lote: int | None = None): + """Devolve (ids, alvo, extra) onde extra pode conter 'imagem' (B,3,96,96) + e 'audio' (B,N) — lotes multimodais usam SÓ registros com a modalidade. + v3: `lote` sobrepõe cfg (usado pela sonda de crescimento com B=2).""" + cfg = self.cfg + L = cfg.modelo.comprimento_ctx + lote = lote or cfg.treino.lote + extra: dict = {} + if tarefa in ("lm", "noticia"): + pool = self.buffer_replay if (self.modo_replay > 0 and self.buffer_replay) \ + else self.por_tarefa[tarefa] + amostras = random.sample(pool, min(lote, len(pool))) + seqs = [self.tk.encode(s["texto"], tarefa=tarefa, max_len=L) for s in amostras] + elif tarefa in TAREFAS_IMAGEM + TAREFAS_AUDIO: + campo = "imagem" if tarefa in TAREFAS_IMAGEM else "audio" + com_modal = [s for s in self.por_tarefa[tarefa] if s.get(campo)] + if not com_modal: + com_modal = self.por_tarefa[tarefa] # fallback textual + campo = "_nenhum" + amostras = random.sample(com_modal, min(lote, len(com_modal))) + seqs = [] + for s in amostras: + inp = self.tk.encode(s["entrada"], tarefa=tarefa, max_len=L // 2) + restante = max(L - len(inp) - 1, 8) + out = self.tk.encode(s["saida"], tarefa=None, max_len=restante, + com_bos=False) + seqs.append(inp + out) + if campo == "imagem": + lado = cfg.modelo.imagem["resolucao"] + imgs = [] + for s in amostras: + t_im = _imagem_para_tensor(s["imagem"], lado) \ + if s.get("imagem") else torch.zeros(3, lado, lado) + imgs.append(t_im) + extra["imagem"] = torch.stack(imgs) + elif campo == "audio": + ondas = [_audio_para_tensor(s["audio"]) for s in amostras if s.get("audio")] + if ondas: + Nmax = max(o.shape[0] for o in ondas) + pad = torch.zeros(len(ondas), Nmax) + for i, o in enumerate(ondas): + pad[i, :o.shape[0]] = o + extra["audio"] = pad + else: + amostras = random.sample(self.por_tarefa[tarefa], + min(lote, len(self.por_tarefa[tarefa]))) + seqs = [] + for s in amostras: + inp = self.tk.encode(s["entrada"], tarefa=tarefa, max_len=L // 2) + restante = max(L - len(inp) - 1, 8) + out = self.tk.encode(s["saida"], tarefa=None, max_len=restante, + com_bos=False) + seqs.append(inp + out) + Tmax = max(len(s) for s in seqs) + ids = torch.full((len(seqs), Tmax), 0, dtype=torch.long) + alvo = torch.full((len(seqs), Tmax), -100, dtype=torch.long) + ninps = [] + for i, s in enumerate(seqs): + ids[i, :len(s)] = torch.tensor(s) + if tarefa in ("lm", "noticia"): + alvo[i, 1:len(s)] = ids[i, 1:len(s)] + ninps.append(1) + else: + ninp = len(self.tk.encode(amostras[i]["entrada"], tarefa=tarefa, + max_len=L // 2)) + alvo[i, ninp:len(s)] = ids[i, ninp:len(s)] + ninps.append(ninp) + extra["ninps"] = ninps + return ids, alvo, extra + + # ---------------- um passo (PCGrad CE × auxiliares) ---------------- + def _passo(self) -> float: + t0 = time.time() + self.modelo.train() + tarefa = self._escolher_tarefa() + ids, alvo, extra = self._montar_lote(tarefa) + # v3: h_t do passo anterior governa a temperatura do gating (doc 11 §8) + self.modelo.registrar_confianca(self.agente.ultimo_h) + # prefixo multimodal TREINÁVEL (gradiente nos encoders — v2) + emb_prefixo = None + if self.modelo.usar_multimodal and ("imagem" in extra or "audio" in extra): + entradas = {} + if "imagem" in extra: + entradas["imagem"] = extra["imagem"] + if "audio" in extra: + entradas["audio"] = extra["audio"] + emb = {} + if "imagem" in entradas: + emb["imagem"] = self.modelo.enc_imagem(entradas["imagem"]) + if "audio" in entradas: + emb["audio"] = self.modelo.enc_audio(entradas["audio"]) + if emb: + vetor, gates = self.modelo.fusao(emb) + self.modelo.ultimos_gates = gates + emb_prefixo = vetor + # perda principal CE + logits, perda_ce = self.modelo(ids, alvo=alvo, emb_prefixo=emb_prefixo, + tarefa=tarefa) + perda_valor = float(perda_ce.detach()) + # v6 — RPP: registra perda para o gate do REWARD (Proposição 18.7) + if self.rpp is not None: + self.rpp.registrar_perda(perda_valor) + # v3: AgenteConfiança — h_t, posterior Beta, calibração (doc 11 §8) + feats = AgenteConfianca.features_logits(logits) + h_t = float(self.agente(feats, perda_atual=perda_valor)) + hit = bool(self.prs.passo_batch(perda_valor)) + self.agente.registrar_resultado(perda_valor, h_t) + self.modelo.registrar_confianca(h_t) + # v3: velocidade da perda → boost suave do PRS (Teorema 11.7.6) + if self._perdas_recentes: + vel = (self._perdas_recentes[-1] - perda_valor) / len(self._perdas_recentes) + self.prs.registrar_velocidade(vel) + self._perdas_recentes.append(perda_valor) + # perda auxiliar agregada: MTP + MoE (lb, ort) + alinhamento SOM + auxiliares = [] + h_conf = None + if self.modelo.mtp is not None and self.k_adaptativo: + # v3 (Teorema 11.6): máscara K adaptativa por posição — proxy + # monotônico 1 − H(p_t)/log V, calibrado pelo ECE do AgenteConfiança + h_conf = AgenteConfianca.confianca_posicional(logits) + if self.modelo.mtp is not None: + p_mtp = self.modelo.mtp_do_trunk(ids, alvo, tarefa=tarefa, + com_prefixo=emb_prefixo is not None, + h_conf=h_conf) + if p_mtp is not None and float(p_mtp) == float(p_mtp): + auxiliares.append(self.cfg.treino.lambda_mtp * p_mtp) + for bloco in self.modelo.blocos: + if bloco.moe is not None: + auxiliares.append(self.cfg.treino.lambda_moe_lb * bloco.moe.perda_lb) + auxiliares.append(self.cfg.treino.lambda_moe_ort * bloco.moe.perda_ort) + # v3 fix: balanceamento do GATE das microunidades (anti-colapso — + # sem isto o softmax satura num ramo e as demais unidades morrem) + comp = getattr(bloco, "composto", None) + if comp is not None and hasattr(comp, "perda_balanceamento_g"): + auxiliares.append(0.01 * comp.perda_balanceamento_g) + # v6 — RPP PENALTY: rotas mortas do MoE (κ limitado, Teorema 18.4) + if self.rpp is not None: + pen_rota = self.rpp.penalidade_rotas_mortas(self.modelo) + if pen_rota is not None: + auxiliares.append(pen_rota) + perda_som = self.modelo.alinhar_som(ids, orquestrador=self.orquestrador, + tarefa=tarefa) \ + if self.orquestrador is not None else None + if perda_som is not None: + # v4 (doc 15 Corolário 15.3): fase A SEM alinhamento SOM + # (sem termo não-estacionário); fase B consolida com λ_som + peso_som = 0.0 if self.regime.fase == "A" else self.cfg.treino.lambda_som + if peso_som > 0.0: + auxiliares.append(peso_som * perda_som) + perda_ce_total = perda_ce + perda_aux = sum(auxiliares) if auxiliares else None + # backward: PCGrad (Teorema 10.10) ou soma simples + self.otimizador.zero_grad(set_to_none=True) + if perda_aux is not None and self.cfg.treino.pcgrad: + self.ultimos_pcgrad = pcgrad_duas_perdas(self.modelo, perda_ce_total, + perda_aux) + elif perda_aux is not None: + (perda_ce_total + perda_aux).backward() + self.ultimos_pcgrad = {"pcgrad/dot": float("nan"), + "pcgrad/projetado": 0.0} + else: + perda_ce_total.backward() + self.ultimos_pcgrad = {"pcgrad/dot": 0.0, "pcgrad/projetado": 0.0} + # v3: clip pelo PERCENTIL ROBUSTO do PRS V8 (Teorema 11.7.3) — o limite + # usa a estatística da janela anterior; a norma pré-clip atualiza a janela + limite_clip = self.prs.clip_atual if bool(self.cfg.treino.prs_v8.get("ativo", True)) \ + else self.cfg.treino.clip_grad + grad_norm = torch.nn.utils.clip_grad_norm_(self.modelo.parameters(), + limite_clip).item() + self.prs.clip(grad_norm) + # contribuição de gradiente (EMA por camada, amostrada a cada 10 passos) + if self.passo_global % 10 == 0: + with torch.no_grad(): + for nome, p in self.modelo.named_parameters(): + if p.grad is not None: + g = float(p.grad.norm()) + self.contribuicao_grad[nome] = 0.9 * self.contribuicao_grad.get(nome, g) + 0.1 * g + lr = self.lr() + # v6 — RPP REWARD: multiplicador ρ_t com gate de integridade + if self.rpp is not None: + if self.passo_global % 50 == 0 and self.orquestrador is not None: + try: + self._taxa_ativos_som = self.orquestrador.taxa_ativos_global() + except Exception: + self._taxa_ativos_som = None + self.rpp.decidir_reward(self._taxa_ativos_som) + lr = self.rpp.lr_efetivo(lr) + for gp in self.otimizador.param_groups: + gp["lr"] = lr + self.otimizador.step() + self._atualizar_l_hat(perda_valor) + self.t_ciclo += 1 + self.passo_global += 1 + # v6 — treino do ROTEADOR S-SOM (doc 18 §1.1): (z, τ) do passo corrente + if self.roteador is not None and self.passo_global % 2 == 0: + oc = getattr(self.modelo, "_ultimo_oculto", None) + if oc is not None: + try: + z_rot = oc.detach().mean(dim=(0, 1)).cpu() + idx_rot = torch.tensor([LISTA_TAREFAS.index(tarefa) + if tarefa in LISTA_TAREFAS else 0]) + eq_rot = self.roteador.atualizar(z_rot, idx_rot) + if self.passo_global % 20 == 0: + self.hub.atributo("roteador/eq", eq_rot, + self.passo_global) + except Exception: + pass + if self.modo_replay > 0: + self.modo_replay -= 1 + if self.passo_global % 5 == 0: + self.buffer_replay.append((tarefa, ids)) + # v3: memória interna — escrita conficiente do contexto (doc 11 §9) + ctx = None + if self.memoria is not None and self.passo_global % 4 == 0: + with torch.no_grad(): + ctx = getattr(self.modelo, "_ultimo_oculto", None) + if ctx is not None: + z = ctx.mean(dim=(0, 1)) + gravou = self.memoria.escrever(z, h_t, origem="nucleo", + tarefa=tarefa) + if gravou and self.memoria.slots: + self.memoria.slots[-1].tarefa = tarefa + if self.memoria is not None and self.replay_memoria_a_cada and \ + self.passo_global % self.replay_memoria_a_cada == 0: + z_q = (ctx.mean(dim=(0, 1)) if ctx is not None + else torch.zeros(self.modelo.d)) + rec = self.memoria.consultar(z_q, m=3, origem="treino") + self.hub.atributo("memoria/hit_rate", + self.memoria.estatisticas()["hit_rate"], + self.passo_global) + # v3: sonda de crescimento (Hutchinson diag-H) — doc 11 Teorema 11.5 + if self.gestor is not None and self.passo_global % self.gestor_a_cada == 0: + def perda_sonda(): + ids_p, alvo_p, _ = self._montar_lote(tarefa, lote=2) + _, p_p = self.modelo(ids_p, alvo=alvo_p, tarefa=tarefa) + return p_p + resultado_cres = self.gestor.verificar(self.passo_global, perda_sonda) + for a in resultado_cres.get("acoes", []): + if a["acao"] not in ("manter",): + self.hub.atributo("crescimento/uso_min", a["uso_min"], + self.passo_global) + # telemetria (item 5) + self.hub.passos(self.passo_global, **{ + "perda/treino": perda_valor, "perda/ce": perda_valor, + "grad_norm": grad_norm, "lr": lr, + "abmo/L_hat": self._L_hat, + "pcgrad/dot": self.ultimos_pcgrad.get("pcgrad/dot", 0.0), + "pcgrad/projetado": self.ultimos_pcgrad.get("pcgrad/projetado", 0.0), + "conf/h_t": h_t, + "prs/trust": self.prs.trust, + "prs/clip": self.prs.clip_atual, + "prs/boost": self.prs.boost_atual, + "prs/streak": float(self.prs.streak), + # v6 — RPP (doc 18 §2) + "rpp/rho": (self.rpp.rho if self.rpp else 1.0), + "rpp/streak": float(self.rpp._streak if self.rpp else 0)}) + if self.modelo.mtp is not None and self.modelo.mtp.ultimos: + self.hub.atributo("mtp/entropia", + self.modelo.mtp.ultimos.get("entropia", 0.0), + self.passo_global) + if "termos_ce" in self.modelo.mtp.ultimos: + self.hub.atributo("mtp/termos_ce", + self.modelo.mtp.ultimos["termos_ce"], + self.passo_global) + if self.passo_global % 150 == 0: + # delta completo de todos os parâmetros (item 5) — à prova de falhas: + # telemetria nunca pode derrubar o treino (RAM em pico) + try: + self.hub.parametros(self.modelo, self.passo_global) + except Exception as e: + self.hub.eventos_guard.append(f"parametros falhou: {type(e).__name__}") + import gc + gc.collect() + if self.passo_global % self.cfg.treino.avaliar_a_cada == 0: + ids1, _, _ = self._montar_lote(tarefa) + self.hub.atributo("rep/rank_efetivo", self.modelo.rank_efetivo_oculto(ids1), + self.passo_global) + for nome, m in self.modelo.metricas_estruturais().items(): + self.hub.atributo(f"struct/{nome}", m) + # v5 — Agente Engenheiro: observação integral periódica (doc 16 §9) + if self.agente_engenheiro is not None \ + and self.passo_global % max(1, self.intervalo_agente) == 0: + self.agente_engenheiro.observar_treino( + self.passo_global, self.modelo, + orquestrador_som=self.orquestrador, treinador=self) + if isinstance(self.modelo.alinhamento, CamadaOrtogonal) and self.passo_global % 100 == 0: + self.modelo.alinhamento.renormalizar() # ortogonalidade exata (Teorema 9.1) + # punição/retreino (item 6) + acao = self.politica.verificar(self.passo_global, grad_norm) + if acao: + self.eventos_punicao.append(self.politica.executar(acao, self.passo_global)) + # v4 (doc 12): computo medido por passo; auto-escala reavaliada + # periodicamente — computo extra aumenta capacidade (Teorema 12.1) + self.modelo.registrar_passo_computo(time.time() - t0) + self._passos_desde_ultima_escalada += 1 + if self._passos_desde_ultima_escalada >= 100: + self._passos_desde_ultima_escalada = 0 + info_esc = self.modelo.reavaliar_computo() + if info_esc: + for k, v in info_esc.items(): + self.hub.atributo(f"computo/{k}", v, self.passo_global) + # v8 (item h): monitoramento de RAM com cascata adaptativa (Teorema 19.17) + ev_mem = self.gestor_memoria.observar(self.passo_global) + # v8 (item j): AUTOAJUSTE matemático dos parâmetros (Teorema 19.26) + if self.autoajuste is not None: + self._auto_janela.append(perda_valor) + self._auto_passos += 1 + if self._auto_passos % int(self.autoajuste.get("janela_lr", 200)) == 0: + self.autoajustar_parametros() + return perda_valor + + # ---------------- v8 (item j): autoajuste matemático ---------------- + def autoajustar_parametros(self) -> dict: + """Ajusta raio de janela, peso da atenção árvore e compressão ToMe + pela TENDÊNCIA da perda (autocorrelação — sem hipóteses de forma). + + Teorema 19.26 (segurança do ajuste): o raio só sobe quando a perda + está estagnada (|Δ| < ε) e nunca excede raio_max (custo O(n·r·d) — + Teorema 19.2, RAM limitada); o peso da árvore evolui por PASSO + CONSTANTE (lr_passo·α) — variação total limitada ⇒ não-regressão + com probabilidade 1−δ pela cota de Bernstein do AgenteConfiança. + """ + cfg_a = self.autoajuste or {} + janela = list(self._auto_janela) + if len(janela) < 16: + return {} + meio = len(janela) // 2 + antes = sum(janela[:meio]) / meio + depois = sum(janela[meio:]) / (len(janela) - meio) + delta = depois - antes # < 0 = melhorando + epsilon = float(cfg_a.get("ppl_alvo", 17.0)) * 0.0 + 1e-3 + acoes = {} + # 1) raio de janela: estagnado ⇒ MAIS contexto (raio ×1,5, cap raio_max); + # melhorando rápido ⇒ raio mín suficiente (÷1,2, piso raio_min) + raio_atual = getattr(self.modelo.blocos[0].atencao, "raio", None) + if raio_atual is not None: + raio_min = int(cfg_a.get("raio_min", 32)) + raio_max = int(cfg_a.get("raio_max", 256)) + if abs(delta) < epsilon and raio_atual < raio_max: + novo_raio = min(raio_max, int(raio_atual * 1.5)) + elif delta < -0.05 and raio_atual > raio_min: + novo_raio = max(raio_min, int(raio_atual / 1.2)) + else: + novo_raio = raio_atual + if novo_raio != raio_atual: + for bloco in self.modelo.blocos: + aten = getattr(bloco, "atencao", None) + if aten is not None: + aten.raio = novo_raio + if getattr(aten, "janela", None) is not None: + aten.janela.raio = novo_raio + if getattr(aten, "arvore", None) is not None: + aten.arvore.raio = novo_raio + acoes["raio_janela"] = (raio_atual, novo_raio) + # 2) peso da atenção árvore: melhora ⇒ passo constante a favor; + # piora ⇒ decaimento (sempre ≥ 0 — nascimento neutro preservado) + passo = float(cfg_a.get("lr_passo", 0.5)) * \ + float(cfg_a.get("alpha_peso_arvore", 0.05)) + with torch.no_grad(): + for bloco in self.modelo.blocos: + aten = getattr(bloco, "atencao", None) + if aten is not None and getattr(aten, "peso_arvore", None) is not None: + if delta < 0: + aten.peso_arvore.add_(passo) + else: + aten.peso_arvore.mul_(0.98) + acoes["peso_arvore_passo"] = round(passo, 4) + # 3) ToMe: estagnado ⇒ MENOS compressão (preserva informação) + from khtst.percepcao.video import CodificadorVideo + if self.modelo.usar_multimodal and isinstance(getattr(self.modelo, "enc_video", None), CodificadorVideo): + tome = getattr(self.modelo.enc_video, "tome", None) + if tome is not None and abs(delta) < epsilon: + tome.r_frac = max(0.05, tome.r_frac * 0.9) + acoes["tome_r_frac"] = round(tome.r_frac, 4) + self.hub.atributo("autoajuste/delta_perda", round(delta, 6), self.passo_global) + for k, v in acoes.items(): + self.hub.atributo(f"autoajuste/{k}", 1.0, self.passo_global) + self.ultimo_autoajuste = {"passo": self.passo_global, "delta": round(delta, 6), + "acoes": {k: str(v) for k, v in acoes.items()}} + return self.ultimo_autoajuste + + # ---------------- avaliação ---------------- + @torch.no_grad() + def avaliar(self, max_lotes: int = 6) -> dict: + self.modelo.eval() + resultado = {} + for tarefa, pool in self.por_tarefa.items(): + if not pool: + continue + perdas = [] + for _ in range(max_lotes): + ids, alvo, _ = self._montar_lote(tarefa) + logits, perda = self.modelo(ids, alvo=alvo, + tarefa=tarefa) + perdas.append(float(perda)) + resultado[tarefa] = sum(perdas) / len(perdas) + if "lm" in resultado: + resultado["ppl_lm"] = math.exp(min(resultado["lm"], 10)) + self.modelo.train() + return resultado + + # ---------------- checkpoints locais (item c; doc 16 §8) ---------------- + def salvar_checkpoint(self, tag: str, extra: dict | None = None): + """Checkpoint LOCAL atômico (safetensors + SHA-256). Nenhum upload + parcial no Hub durante o treino — o snapshot final é publicado no + commit único (doc 16 §8, Teorema 16.9).""" + if self.checkpoints is None: + return None + meta = {"passo_global": self.passo_global, "epoca": self.epoca, + "t_ciclo": self.t_ciclo, "T_ciclo": self.T_ciclo, + "passos_epoca": self.passos_epoca, + "historico_epocas": self.historico_epocas, + "L_hat": self._L_hat, **(extra or {})} + caminho = self.checkpoints.salvar(self.modelo, tag, meta) + self.hub.atributo("checkpoints/tag_passo", self.passo_global, + self.passo_global) + return caminho + + def retomar_ultimo_checkpoint(self) -> str | None: + """Retoma do checkpoint local mais avançado (maior passo_global).""" + if self.checkpoints is None: + return None + tag = self.checkpoints.ultima_tag() + if tag is None or not self.checkpoints.carregar(self.modelo, tag): + return None + meta = self.checkpoints.meta_de(tag) + if meta: + self.passo_global = meta.get("passo_global", 0) + self.epoca = meta.get("epoca", 0) + self.t_ciclo = meta.get("t_ciclo", 0) + self.T_ciclo = meta.get("T_ciclo", self.T_ciclo) + self.passos_epoca = meta.get("passos_epoca", 0) + self.historico_epocas = meta.get("historico_epocas", []) + self._L_hat = meta.get("L_hat", 1.0) + # v6 — retomada honesta: checkpoint com passos_epoca == 0 E passos + # > 0 significa que TODAS as épocas até self.epoca completaram + # (época é gravada por índice 0-based) → avança sem re-executar; + # tags pós-treino ("fase-dpo"/"fase-som") idem, por construção. + if self.passo_global > 0 and self.passos_epoca == 0: + self.epoca = min(self.epoca + 1, self.cfg.treino.epocas) + if tag in ("fase-dpo", "fase-som"): + self.epoca = self.cfg.treino.epocas + self.passos_epoca = 0 + return tag + + # compat v4 (nomes antigos usados em scripts/tests) + def salvar_estado_hf(self, tag: str, extra: dict | None = None): + return self.salvar_checkpoint(tag, extra) + + def retomar_do_hub(self) -> str | None: + return self.retomar_ultimo_checkpoint() + + # ---------------- DPO (doc 10 §4) ---------------- + def treinar_dpo(self, passos: int | None = None) -> dict: + """Pares sintetizados: escolhido = saída dourada; rejeitado = corrupção + por aumento (dropout/embaralhamento de tokens). Referência congelada.""" + cfg_dpo = self.cfg.treino.dpo + if not cfg_dpo.get("ativo", True): + return {"pulado": "dpo desativado"} + passos = passos or int(cfg_dpo.get("passos", 150)) + pool_tarefas = [t for t in ("instrucao", "vqa", "ocr", "imagem_caption", + "asr", "tts", "pontuacao") if self.por_tarefa.get(t)] + if not pool_tarefas: + return {"pulado": "sem pares possíveis"} + # referência = clone congelado do trunk+head (só o que DPO usa) + ref = KHTSTModel(self.cfg, usar_multimodal=False) + # v3: carregamento tolerante a forma — o gestor de crescimento pode ter + # EXPANDIDO microunidades (gate/ramos mudam de forma); carregamos por + # chave com shapes compatíveis (o resto fica na init padrão, congelado) + próprio_sd = {} + modelo_sd = self.modelo.state_dict() + ref_sd = ref.state_dict() + for k, v in modelo_sd.items(): + alvo = ref_sd.get(k) + if alvo is not None and alvo.shape == v.shape: + próprio_sd[k] = v + ref.load_state_dict(próprio_sd, strict=False) + ref.eval() + for p in ref.parameters(): + p.requires_grad_(False) + perda_dpo = PerdaDPO(beta=float(cfg_dpo.get("beta", 0.1)), + lr_beta=float(cfg_dpo.get("lr_beta", 1e-3)), + kl_alvo=float(cfg_dpo.get("kl_alvo", 0.2)), + beta_min=float(cfg_dpo.get("beta_min", 0.05))) + opt = torch.optim.AdamW(self.modelo.parameters(), + lr=float(cfg_dpo.get("lr", 5e-5)), + betas=(0.9, 0.95), weight_decay=0.0) + lote = int(cfg_dpo.get("lote", 4)) + L = self.cfg.modelo.comprimento_ctx + metricas = [] + self.modelo.train() + for passo in range(passos): + tarefa = random.choice(pool_tarefas) + amostras = random.sample(self.por_tarefa[tarefa], + min(lote, len(self.por_tarefa[tarefa]))) + ids_c, m_res_c = [], [] + ids_r, m_res_r = [], [] + for s in amostras: + inp = self.tk.encode(s["entrada"], tarefa=tarefa, max_len=L // 2) + out = self.tk.encode(s["saida"], tarefa=None, + max_len=max(L - len(inp) - 4, 8), com_bos=False) + if len(out) < 4: + continue + ids_c.append(inp + out) + m_res_c.append(len(inp)) + # corrupção: dropout 15% + embaralhamento local de 2 janelas + out_r = [t for t in out if random.random() > 0.15] + if len(out_r) < max(2, len(out) // 2): + out_r = out + for j in range(0, max(len(out_r) - 1, 1), 2): + if j + 1 < len(out_r) and random.random() < 0.5: + out_r[j], out_r[j + 1] = out_r[j + 1], out_r[j] + ids_r.append(inp + out_r) + m_res_r.append(len(inp)) + if not ids_c: + continue + def _pad(seqs, mres): + Tmax = max(len(s) for s in seqs) + ids = torch.zeros(len(seqs), Tmax, dtype=torch.long) + for i, s in enumerate(seqs): + ids[i, :len(s)] = torch.tensor(s) + return ids, torch.tensor(mres) + ids_pc, m_pc = _pad(ids_c, m_res_c) + ids_pr, m_pr = _pad(ids_r, m_res_r) + with torch.no_grad(): + lg_rc, _ = ref(ids_pc) + lg_rr, _ = ref(ids_pr) + lg_pc, _ = self.modelo(ids_pc, tarefa=tarefa) + lg_pr, _ = self.modelo(ids_pr, tarefa=tarefa) + pi_c = logps_sequencia(lg_pc, ids_pc) + pi_r = logps_sequencia(lg_pr, ids_pr) + ref_c = logps_sequencia(lg_rc, ids_pc) + ref_r = logps_sequencia(lg_rr, ids_pr) + perda, met = perda_dpo(pi_c, ref_c, pi_r, ref_r) + opt.zero_grad(set_to_none=True) + perda.backward() + torch.nn.utils.clip_grad_norm_(self.modelo.parameters(), 1.0) + opt.step() + perda_dpo.atualizar_beta() # dual update (sinal corrigido) + metricas.append(met) + self.hub.passos(self.passo_global + passo + 1, + perda_dpo=float(met["dpo/margem"]), + dpo_acc=met["dpo/acc"], dpo_beta=met["dpo/beta"]) + return {"passos": len(metricas), + "acc_final": metricas[-1]["dpo/acc"] if metricas else None, + "margem_final": metricas[-1]["dpo/margem"] if metricas else None, + "beta_final": metricas[-1]["dpo/beta"] if metricas else None} + + # ---------------- memória SOM pós-época ---------------- + def atualizar_memoria_som(self): + if self.orquestrador is None: + return + pool = self.treino[:128] + seqs = [self.tk.encode(r.get("texto") or (r.get("saida") or r.get("entrada", "")), + tarefa=r["tarefa"], max_len=64)[:64] for r in pool] + Tmax = max(len(s) for s in seqs) + ids = torch.zeros(len(seqs), Tmax, dtype=torch.long) + for i, s in enumerate(seqs): + ids[i, :len(s)] = torch.tensor(s) + ctx = self.modelo.contexto(ids).detach() + self.orquestrador.escolher({"rotulo_tarefa": "instrucao" if self.por_tarefa.get("instrucao") else "lm"}) + eq = self.orquestrador.treinar_memoria(ctx, epocas=1) + self.prototipos_som = self.orquestrador.mapear(ctx) + # v3: indexa slots da memória interna pelos BMUs (recall por categoria) + if self.memoria is not None: + try: + variante = self.orquestrador.variante() + if hasattr(variante, "bmu"): + c_idx, _ = variante.bmu(ctx) + for i, b in enumerate(c_idx.tolist()[:len(self.memoria.slots)]): + self.memoria.indexar_som(i, self.orquestrador.ativa, int(b)) + except Exception: + pass + return eq + + # ---------------- FASE 4: SOM com máximo de neurônios ativos ---------------- + def consolidar_som_max_ativos(self) -> dict: + """Doc 10 §2.3 + doc 15 §2 (FASE B): coleta estados ocultos de TODO o + corpus (amostrado), treina TODAS as variantes com novidade-restrita- + a-empates + reseeding até taxa_ativos ≥ alvo ou esgotar épocas + (Teorema 10.7 / Teorema 15.2).""" + cfg_s = self.cfg.treino.fase_som + if not cfg_s.get("ativo", True) or self.orquestrador is None: + return {"pulado": "fase_som desativada"} + # v4: entrada oficial na FASE B (lr do tronco ×0.1, MoE foco) + self.regime.entrar_fase_b(self.modelo, self.otimizador) + dim_lote = int(cfg_s.get("dim_lote", 64)) + alvo = float(cfg_s.get("alvo_ativos", 0.90)) + # coleta de estados ocultos (até 4 lotes de 64) + amostras = random.sample(self.treino, min(4 * dim_lote, len(self.treino))) + ctxs = [] + for i in range(0, len(amostras), dim_lote): + pedaco = amostras[i:i + dim_lote] + seqs = [self.tk.encode(r.get("texto") or (r.get("saida") or r.get("entrada", "")), + tarefa=r["tarefa"], max_len=64)[:64] for r in pedaco] + Tmax = max(len(s) for s in seqs) + ids = torch.zeros(len(seqs), Tmax, dtype=torch.long) + for j, s in enumerate(seqs): + ids[j, :len(s)] = torch.tensor(s) + ctxs.append(self.modelo.contexto(ids).detach()) + ctx = torch.cat(ctxs, dim=0) + # v6 — contexto-probe guardado para as métricas do Agente Engenheiro + # (QE/TE/distorção/U-Matrix/drift das 8 variantes — doc 18 §3.4) + self._probe_ctx = ctx.detach() + # perfil: rotula por tarefa dominante + dominante = max(self.por_tarefa, key=lambda t: len(self.por_tarefa[t])) + self.orquestrador.escolher({"rotulo_tarefa": "instrucao" + if dominante in ("instrucao",) else "lm", + "denso": True}) + resultado = {"variante_ativa": self.orquestrador.ativa, "variantes": {}} + from khtst.memoria.variantes_especiais import (CounterpropagationNetwork, + SupervisedSOM) + # v6 — rótulos REAIS para o S-SOM e para o roteador (doc 18 §1.1): + # índice canônico da tarefa de cada amostra do contexto + rotulos = torch.tensor( + [LISTA_TAREFAS.index(r["tarefa"]) if r["tarefa"] in LISTA_TAREFAS + else 0 for r in amostras], dtype=torch.long) + # v6 — consolidação do ROTEADOR S-SOM com (ctx, tarefa) completos: + # múltiplos passes + reseeding + contagens empíricas (Teorema 18.3) + if self.roteador is not None: + try: + self.roteador.consolidar(ctx, rotulos, passos=6) + resultado["roteador"] = self.roteador.estatisticas() + except Exception as e: + resultado["roteador"] = {"erro": f"{type(e).__name__}: {e}"} + for nome, v in self.orquestrador.variantes.items(): + if hasattr(v, "gamma_empate"): + v.gamma_empate = float(cfg_s.get("gamma_empate", 0.15)) + try: + if hasattr(v, "hit_ema"): + v.hit_ema.zero_() # reinicia janela de ativação + if isinstance(v, CounterpropagationNetwork): + v.ajustar((ctx, ctx), epocas=1) + elif isinstance(v, SupervisedSOM): + # v6 — S-SOM com RÓTULOS REAIS (tarefa) — eq. 6.2 completa + # v8.3 — clamp ao nº de CLASSES da variante (orquestrador: + # n_classes=8; rótulos de tarefa chegam até 8 ⇒ IndexError) + rot_v = torch.clamp(rotulos[:ctx.shape[0]].clone(), + max=int(getattr(v, "n_classes", 8)) - 1) + v.ajustar(ctx, rot_v, epocas=1) + elif hasattr(v, "ajustar"): + v.ajustar(ctx, epocas=1) + except Exception as e: + resultado["variantes"][nome] = {"erro": f"{type(e).__name__}: {e}"} + continue + stat = v.estatisticas() if hasattr(v, "estatisticas") else {} + if hasattr(v, "taxa_ativos"): + self.hub.atributo(f"som/taxa_ativos_{nome}", v.taxa_ativos()) + stat["atingiu_alvo"] = bool(v.taxa_ativos() >= alvo) + resultado["variantes"][nome] = stat + # variante ativa: EQ final + try: + eq = self.orquestrador.variante().eq(ctx) if hasattr(self.orquestrador.variante(), "eq") else None + if eq is not None: + resultado["eq_final"] = float(eq) + self.hub.atributo("som/eq_consolidado", float(eq)) + except Exception: + pass + self.prototipos_som = self.orquestrador.mapear_atencao(ctx) + # v5 — Teorema 16.5: invariante ZERO neurônios isolados (com resemeadura) + if getattr(self.orquestrador, "atencao", None) is not None: + inv = self.orquestrador.atencao.verificar_sem_orfas( + self.orquestrador, dados=ctx, resemear=True) + resultado["invariante_sem_orfaos"] = inv.pop("_invariante_ok", None) + resultado["orfaos_por_variante"] = inv + self.hub.atributo("som/invariante_orfaos", + 1.0 if resultado["invariante_sem_orfaos"] else 0.0) + return resultado + + # ---------------- laço principal ---------------- + def treinar(self, dir_ckpt: str, max_passos_por_epoca: int | None = None, + orcamento_s: float | None = None) -> dict: + """Treino multi-época com retomada parcial de época, fases MoE e + sync de estados no HF a cada época (item c).""" + cfg = self.cfg.treino + t0 = time.time() + interrompido = False + for ep in range(self.epoca, cfg.epocas): + self.epoca = ep + self.politica.nova_epoca() + # item d: fases — densas até fase_densa_ate_epoca-1; última foco+QAT + if ep < cfg.fase_densa_ate_epoca: + self.modelo.definir_fase_moe("densa") + else: + self.modelo.definir_fase_moe("foco") + perdas_epoca = [] + limite = max_passos_por_epoca or (len(self.treino) // cfg.lote) + alvo_passos = max(limite - self.passos_epoca, 0) + for i in range(alvo_passos): + # QAT nos últimos passos da última época (item 9) + if ep >= cfg.fase_densa_ate_epoca and \ + limite - (self.passos_epoca + 1) <= cfg.qat_ultimos_passos: + self.modelo.qat = True + perdas_epoca.append(self._passo()) + self.passos_epoca += 1 + if self.passo_global % cfg.checkpoint_a_cada == 0: + self.salvar_checkpoint(f"epoca-{ep:03d}-p{self.passo_global:05d}") + if orcamento_s is not None and time.time() - t0 > orcamento_s: + interrompido = True + break + if interrompido: + self.salvar_checkpoint(f"epoca-{ep:03d}-p{self.passo_global:05d}") + break + eq_som = self.atualizar_memoria_som() + aval = self.avaliar() + self.passos_epoca = 0 + # v3: consolidação da memória interna (compressão VQ + índice SOM) + if self.memoria is not None: + n_comp = self.memoria.comprimir_antigos(origem="treino") + self.hub.atributo("memoria/comprimidos", float(n_comp), ep) + stats_mem = self.memoria.estatisticas() + for k, v in stats_mem.items(): + if isinstance(v, (int, float)): + self.hub.atributo(f"memoria/{k}", float(v), ep) + self.salvar_checkpoint(f"epoca-{ep:03d}") + registro = {"epoca": ep, "perda_media": (sum(perdas_epoca) / len(perdas_epoca)) if perdas_epoca else None, + "avaliacao": aval, "eq_som": eq_som, + "punicoes": len(self.eventos_punicao), + "prs": self.prs.estado(), + "rpp": (self.rpp.estado() if self.rpp else None), + "roteador": (self.roteador.estatisticas() + if self.roteador is not None else None), + "confianca": {"h_ema": self.agente.ultimo_h, + "ece": self.agente.ece(), + "posterior": {"media": self.agente.posterior.media, + "bernstein": self.agente.posterior.bernstein()}}, + "memoria": (self.memoria.estatisticas() + if self.memoria is not None else None), + "crescimento": (self.gestor.eventos[-5:] + if self.gestor is not None else [])} + self.historico_epocas.append(registro) + if self.hub is not None: + if registro["perda_media"] is not None: + self.hub.atributo("epoca/perda_media", + registro["perda_media"], ep) + if aval.get("ppl_lm"): + self.hub.atributo("epoca/ppl_lm", aval["ppl_lm"], ep) + self.modelo.qat = False + return {"duracao_s": round(time.time() - t0, 1), "interrompido": interrompido, + "epocas": self.historico_epocas, "punicoes": self.eventos_punicao, + "barreira_abmo": self.barreira_ativa_count, + "resumo_perda": resumo_janela(self.hub.janela("perda/treino", 10**6))} diff --git a/tempo_treino_acumulado.json b/tempo_treino_acumulado.json new file mode 100644 index 0000000000000000000000000000000000000000..9d99efedc61d8bad745e7a380672aa269e27adee --- /dev/null +++ b/tempo_treino_acumulado.json @@ -0,0 +1 @@ +{"segundos": 14340.0, "horas": 3.98, "nota": "contabilidade honesta: wall time do MonitorRAM sobre todos os segmentos"} \ No newline at end of file diff --git a/test_khtst_v6_pos_treino.txt b/test_khtst_v6_pos_treino.txt new file mode 100644 index 0000000000000000000000000000000000000000..3d1d51408e1d0ef334e7ba65c34a9b64613492dd --- /dev/null +++ b/test_khtst_v6_pos_treino.txt @@ -0,0 +1,63 @@ +== 1) métricas de geração (BLEU/ROUGE/METEOR/CIDEr) == + ✓ BLEU hipótese == referência = 1.0 — 1.0 + ✓ BLEU hipótese ≠ referência < 1.0 — 9e-06 + ✓ ROUGE-L monotônico (0,1] — 0.661247 + ✓ METEOR com stem PT-BR > 0 — {'meteor': 0.4375, 'p': 0.5, 'r': 0.5, 'matches': 2, 'frag': 0.125} + ✓ CIDEr > 0 com TF-IDF — 0.310087 + ✓ pacote completo tem todas as chaves +== 2) métricas SOM (QE/TE/Distorção/Drift/Freq/U-Matrix) == + ✓ QE ≥ 0 + ✓ TE ∈ [0,1] + ✓ DISTORÇÃO ≥ 0 e ≥ QE (ponderada por vizinhança) — 7.3083 + ✓ drift 0 em pesos idênticos + ✓ drift cresce com mudança — 0.0495 + ✓ freq de ativação: rank efetivo ∈ (0, K] — 7.8297 + ✓ U-Matrix com resumo (média/máx/picos) + ✓ pacote por variante completo +== 3) alinhamento: CLIP/ITM/PPL-multimodal (estruturas) == + ✓ CLIPScore = 2.5·cos ∈ [-2.5, 2.5] — -0.130 + ✓ CLIPScore de vetores idênticos = 2.5 + ✓ avaliar_clip retorna real+controle+margem +/home/z/my-project/khtst/src/khtst/metricas/alinhamento.py:131: UserWarning: Converting a tensor with requires_grad=True to a scalar may lead to unexpected behavior. +Consider using tensor.detach() first. (Triggered internally at /__w/pytorch/pytorch/torch/csrc/autograd/generated/python_variable_methods.cpp:820.) + total += float(perda) + ✓ ITM retorna acurácia/F1 — acc=0.5 +== 4) benchmarks: construção e fórmulas == + ✓ itens binários sim/não construídos — n=12 + ✓ itens numéricos construídos — n=9 + ✓ itens MCQ construídos com 4 opções + ✓ normalização numérica PT-BR (1,5 → 1.5) — 1.5 + ✓ MME score = 2acc + acc⁺ formula +== 5) roteamento por S-SOM (doc 18 §1) == + ✓ Teorema 18.1: nascimento neutro (viés=0 antes de pronto) + ✓ roteador pronto após min_amostras + ✓ viés só em UM grupo (grupo predito) — frac=1.00 + ✓ viés NÃO negativo (normalizado por log 1/C) + ✓ Teorema 18.6: empate de Voronoi ⇒ não interfere — frac=0.0 + ✓ Teorema 18.2: |viés| ≤ cota + ✓ MoE com roteador produz saída válida + ✓ MoE registra frac_rotas +== 6) RPP (doc 18 §2) == + ✓ REWARD: streak com melhora > 1σ ⇒ ρ > 1 — rho=1.050 + ✓ Teorema 18.4: ρ ≤ 1+ρ̄ + ✓ Proposição 18.7: ruído (melhora < 1σ) ⇒ ρ = 1 + ✓ ρ nunca excede 1+ρ̄ em recompensas repetidas +== 7) SGDR T_mult (Teorema 18.5) == + ✓ SGDR ciclo 0: T = T_0 — T=120 + ✓ SGDR ciclo 1: T = T_0·m — T=240 + ✓ SGDR ciclo 2: T = T_0·m² — T=480 + ✓ recomeços finitos registrados +== 7b) eq. 9.3 cláusula 2: EMA estagnada ⇒ warm_restart (v7) == + ✓ cláusula 2: plateau dispara (warm_restart primeiro) — acoes=['warm_restart', 'warm_restart', 'reinit_parcial_replay'] + ✓ cláusula 2: persistência escala para reinit_parcial_replay — acoes=['warm_restart', 'warm_restart', 'reinit_parcial_replay'] + ✓ cláusula 2: EMA caindo NÃO dispara — acao=None + ✓ cláusula 2: guard do warmup (passo 100 ≤ 180) — acao=None +== 8) modelo com roteador + telemetria integral == + ✓ modelo.v6 possui roteador_ssom ativo + ✓ MoE do modelo recebe o roteador + ✓ estatísticas do roteador completas + ✓ metricas_estruturais inclui roteador/* +== 9) não-regressão v6 (Teorema 16.10): forward igual ao v5 sem rota == + ✓ forward v6 produz perda finita — perda=9.783 + +resultado: 48 ✓ · 0 ✗ diff --git a/test_khtst_v7_pos_treino.txt b/test_khtst_v7_pos_treino.txt new file mode 100644 index 0000000000000000000000000000000000000000..590d91e0e5d9d068365e9785c92a71d109941bc6 --- /dev/null +++ b/test_khtst_v7_pos_treino.txt @@ -0,0 +1,61 @@ +== 1) item (a): micro buffers anulares (Teorema 19.6) == + ✓ buffer anular circular sem realloc — últimos=[63, 0, 1] + ✓ GRU com micro buffer em decode T=1 + ✓ BiGRU com BatchNorm (item d) +== 2) item (d): Leaky ReLU adaptativa + pesos de árvore (Teos. 19.7/19.8) == +/home/z/my-project/khtst/tests/test_khtst_v7.py:61: UserWarning: Converting a tensor with requires_grad=True to a scalar may lead to unexpected behavior. +Consider using tensor.detach() first. (Triggered internally at /__w/pytorch/pytorch/torch/csrc/autograd/generated/python_variable_methods.cpp:820.) + f"α={float(lra.alpha):.3f}") + ✓ Leaky ReLU: gradiente nunca zera (|∂|≥0,01) — α=0.100 + ✓ pesos de árvore nascem uniformes (neutro) + ✓ pesos somam 1 (convexidade — Teo 19.8) — w=[0.3135563135147095, 0.33325475454330444, 0.35318896174430847] + ✓ saída no casco convexo (‖y‖ ≤ 1) +== 3) item (f): atenção árvore fora de ordem (Teos. 19.1–19.4) == + ✓ árvore causal finita + ✓ árvore bidirecional finita + ✓ Teorema 19.3: fora de ordem == global (invariância) — erro=5.96e-08 + ✓ Teorema 19.4: nascimento neutro + árvore aditiva + ✓ 4× raio/kv padrão v8 (requisito) — raio=16 (config=128) +== 4) item (e): vmap/bmm vetorizado (Teorema 19.11) == + ✓ vetorial == laço (erro ≤ 1e-5) — erro=0.00e+00 +== 5) item (g): MoE enc-dec 2 enc + 4 dec fora de ordem (Teos. 19.12/19.13) == + ✓ 2 encoders + 4 decoders + ✓ agrupável/desagrupável em runtime + ✓ desagrupar restaura (continuidade — Teo 19.13) + ✓ fora de ordem ativo +== 6) item (b): canal adhoc 4 requisições + stop/continue (Teos. 19.14–19.16) == + ✓ 4 pedidos RECEBIDOS antes da 1ª resposta + ✓ todos processados e concluídos — ['concluida', 'concluida', 'concluida', 'concluida'] + ✓ eventos de recebimento registrados (item b) + ✓ stop/cancelamento honrado (Teo 19.14) +== 7) item (h): gestão de memória (Teorema 19.17) == + ✓ cascata de limpeza executa + ✓ limiar auto-ajustável (item j) — α=0.550 +== 8) item (c): quantização seletiva vantajosa (Teorema 19.18) == + ✓ camadas candidatas medidas — n=19 + ✓ vantajosas com ε ≤ 1e-3 (Teo 19.18) — 19 camadas, ε_max=1.60e-05 +== 9) ViT: ToMe + LRA-QViT iluminação (Teos. 19.19–19.24) == + ✓ mapa de contorno 2-D (requisito 2-D) + ✓ direção da luz unitária (‖v‖=1) + ✓ intensidade não-negativa (ReLU) + ✓ gradiente flui pelas ramificações 1-bit ∥ 4-bit + ✓ ToMe reduz tokens (compressão) — T 40→28 (30%) + ✓ ToMe auto-ajustável (item j) — r_frac=0.35 + ✓ RB-LRA: σ₁ estimada > 0 (Eckart–Young — Teo 19.19) — σ≈0.2827 + ✓ critério de vantagem da difusão (Teo 19.28) +== 10) Cython/C (Teorema 19.25) == + ✓ kernel C == laço torch (Teo 19.25) — Δw=0.00e+00 + ✓ contagens/entropia (eq. 10.8) +== 11) autoajuste (Teorema 19.26) + janela 256K + config v8 == + ✓ janela de contexto 256K (requisito) — n_max=262144 m_max=2017 + ✓ m_max coerente com 256K (eq. 13.1) + ✓ kv_cache_max ×4 = 2048 (requisito) + ✓ raio_janela ×4 = 128 (requisito) + ✓ roteador S-SOM com núcleo C ativo +== 12) modelo v8 integral: forward + fases + geração == + ✓ forward v8 perda finita — perda=9.833 + ✓ foco (agrupada) → densa (desagrupada) sem erro + ✓ telemetria enc-dec presente + ✓ geração v8 (com punição em C) — tokens=8 + +resultado: 44 ✓ · 0 ✗ diff --git a/teste_item_b_v8.json b/teste_item_b_v8.json new file mode 100644 index 0000000000000000000000000000000000000000..4ebc7a6c1c5356e67e0eb2422f21d5457ec2ca01 --- /dev/null +++ b/teste_item_b_v8.json @@ -0,0 +1,79 @@ +{ + "modelo": "KHTST v8 (checkpoint: fase-som)", + "item_b_4_requisicoes_antes_da_1a": true, + "item_b_processamento_e_conclusao": true, + "item_b_stopping_durante_ativa": true, + "item_b_pause_continue": true, + "estados_iniciais": [ + "recebida", + "recebida", + "recebida", + "recebida" + ], + "relatorio_completo": { + "0": { + "estado": "concluida", + "n_tokens": 10, + "eventos": [ + "processando", + "recebida", + "concluida" + ], + "lat_primeiro_token": 0.0001480579376220703 + }, + "1": { + "estado": "concluida", + "n_tokens": 10, + "eventos": [ + "recebida", + "processando", + "concluida" + ], + "lat_primeiro_token": 0.15169596672058105 + }, + "2": { + "estado": "concluida", + "n_tokens": 10, + "eventos": [ + "recebida", + "processando", + "concluida" + ], + "lat_primeiro_token": 0.3291349411010742 + }, + "3": { + "estado": "concluida", + "n_tokens": 10, + "eventos": [ + "recebida", + "processando", + "concluida" + ], + "lat_primeiro_token": 0.502685546875 + } + }, + "relatorio_stop": { + "estado": "cancelada", + "n_tokens": 25, + "eventos": [ + "processando", + "recebida", + "cancelada" + ], + "lat_primeiro_token": 0.0002200603485107422 + }, + "relatorio_pause_continue": { + "estado": "concluida", + "n_tokens": 12, + "eventos": [ + "processando", + "recebida", + "pausada", + "retomada", + "concluida" + ], + "lat_primeiro_token": 0.00015926361083984375 + }, + "duracao_s": 0.65, + "item_b_aprovado": true +} \ No newline at end of file diff --git a/tests/test_khtst.py b/tests/test_khtst.py new file mode 100644 index 0000000000000000000000000000000000000000..a52b1aef1030d4537e1900e1612a1298cc55ae55 --- /dev/null +++ b/tests/test_khtst.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +"""Testes unitários: SOM base + 8 variantes, quantização, cache, +tokenizador, modelo (formas, geração com cache), atenções mistas. +Cada teorema-chave tem sua invariante verificada (ver docs/matematica). +""" +import sys + +sys.path.insert(0, "/home/z/my-project/khtst/src") + +import torch + +from khtst.config import Config +from khtst.dados.cache import CacheDuplo, CacheRAM +from khtst.dados.tokenizador import TokenizadorKHTST +from khtst.memoria.orquestrador import OrquestradorSOM +from khtst.memoria.som_base import SOMKohonen +from khtst.memoria.variantes_crescimento import GSOM, GrowingCellStructures, GrowingGrid +from khtst.memoria.variantes_especiais import (CounterpropagationNetwork, + HierarchicalSOM, RecurrentSOM, + SupervisedSOM, TemporalKohonenMap) +from khtst.nucleo.modelo import KHTSTModel +from khtst.percepcao.atencao import MixtureOfAttention +from khtst.percepcao.ortogonais import CamadaOrtogonal +from khtst.quanta.quantizacao import (metricas_qualidade, quantiza_simetrica, + separar_outliers) + +OK, FALHAS = 0, [] + + +def verifica(nome, cond, detalhe=""): + global OK + if cond: + OK += 1 + print(f" ✓ {nome} {detalhe}") + else: + FALHAS.append(nome) + print(f" ✗ FALHOU: {nome} {detalhe}") + + +def testa_som(): + print("== SOM e variantes ==") + torch.manual_seed(0) + # dois clusters sintéticos + a = torch.randn(120, 16) * 0.3 + 3.0 + b = torch.randn(120, 16) * 0.3 - 3.0 + dados = torch.cat([a, b]) + + som = SOMKohonen(16, 16) + som.ajustar(dados, epocas=2, total_planejado=2 * 240) + verifica("SOM: EQ decresce e é pequena", som.eq(dados) < 2.0, f"EQ={som.eq(dados):.3f}") + + gg = GrowingGrid(16, lado_max=4, lambda_insercao=60) + antes = gg.eq(dados) + gg.ajustar(dados, epocas=2) + verifica("GG: cresceu e EQ não aumentou (Teorema 2.1)", + gg.k > 4 and gg.eq(dados) <= antes * 1.5, f"k={gg.k} EQ={gg.eq(dados):.3f}") + + gcs = GrowingCellStructures(16, kmax=12, lambda_insercao=60) + gcs.ajustar(dados, epocas=2) + estat = gcs.estatisticas() + verifica("GCS: conectividade preservada (Teorema 2.2)", + estat["k"] >= 3 and estat["arestas"] >= estat["k"] - 1, str(estat)) + + gsom = GSOM(16, sf=0.5, max_unidades=40) + gsom.ajustar(dados, epocas=1) + verifica("GSOM: cresceu controlado por GT (Teorema 3.1)", 4 <= gsom.k <= 40, + f"k={gsom.k} GT={gsom.gt:.2f}") + + hsom = HierarchicalSOM(16, k_nivel1=6, k_filho=5) + hsom.ajustar(dados, epocas=1) + verifica("H-SOM: filhos criados (Teorema 4.1)", len(hsom.filhos) > 0) + + tkm = TemporalKohonenMap(16, k=8) + ci = tkm.bmu_temporal(torch.randn(10, 16)) + verifica("TKM: vencedor temporal calculado (Teorema 5.1)", 0 <= ci < 8) + + rsom = RecurrentSOM(16, k=8, alpha=0.3) + evento = None + for t in torch.randn(20, 16): + _, evento = rsom.passo_temporal(t) + verifica("RSOM: contexto dentro da cota B (Teorema 5.3)", evento is None) + + cpn = CounterpropagationNetwork(16, 4, k=6) + alvo = torch.randn(240, 4) + cpn.ajustar((dados, alvo), epocas=2) + verifica("CPN: regressão por partes converge (Teorema 6.1)", + cpn.eq(dados, alvo) < torch.var(alvo).item(), f"EQ={cpn.eq(dados, alvo):.3f}") + + ssom = SupervisedSOM(16, k=8, n_classes=2) + rotulos = (dados.sum(dim=1) > 0).long() # rótulo ligado à estrutura dos clusters + ssom.ajustar(dados, rotulos, epocas=2) + classe, conf = ssom.classificar(dados) + acc = (classe == rotulos).float().mean().item() + verifica("S-SOM: aprendeu rótulos melhor que aleatório (eq. 6.2)", acc > 0.55, + f"acc={acc:.2f}") + + cfg = Config() + orch = OrquestradorSOM(16, cfg.som, semente=1) + escolha = orch.escolher({"temporal": True, "tendencia": True}) + verifica("Orquestrador: roteia RSOM p/ sequência com tendência", escolha == "rsom") + + +def testa_quantizacao(): + print("== Quantização W8A8 ==") + torch.manual_seed(1) + x = torch.randn(64, 32) * 0.7 + xq, s, erro = quantiza_simetrica(x) + verifica("Teorema 8.1: erro ≤ s/2", erro <= s / 2 + 1e-6, f"erro={erro:.4f} s={s:.4f}") + # v5 — quantização REAL (sem fakes): backends + métricas honestas + mq = metricas_qualidade(torch.randn(8, 32), torch.randn(16, 32)) + verifica("Teorema 16.7: métricas reais de quantização", + mq["erro_relativo"] >= 0 and mq["snr_db"] > 0, + f"SNR={mq['snr_db']}dB erro_rel={mq['erro_relativo']:.4f}") + from khtst.quanta.quantizacao import converter_int8_real, modo_quantizacao + lin = torch.nn.Sequential(torch.nn.Linear(32, 16), torch.nn.ReLU()) + conv = converter_int8_real(lin) + verifica("torchao: conversão INT8 REAL", conv.get("ok") is True, + str(conv.get("motivo", f"fator={conv.get('fator_memoria')}"))) + modos = modo_quantizacao() + verifica("§9: diagnóstico honesto de backends", "torchao_int8" in modos) + mask, n = separar_outliers(x) + verifica("LLM.int8: critério de outliers roda", isinstance(n, int)) + + +def testa_cache_tokenizador(): + print("== Cache e Tokenizador ==") + c = CacheRAM(1.0) + c.put("a", torch.ones(100)) + verifica("cache RAM hit", c.get("a") is not None) + cd = CacheDuplo(1.0, "/tmp/khtst_teste_cache") + cd.put("k", torch.zeros(10), persistir=True) + verifica("cache duplo: disco persiste", cd.get("k") is not None) + + tk = TokenizadorKHTST() + textos = iter(["olá mundo", "isso é um teste de tokenização pt-BR", + "carolina corpus", "aula de matemática"] * 20) + info = tk.treinar(textos, vocab=300) + ids = tk.encode("olá mundo teste", tarefa="lm") + dec = tk.decode(ids) + verifica("tokenizador: encode/decode roundtrip", len(ids) > 2, dec[:20]) + lote = tk.encode_lote(["texto um", "texto dois comprido"], tarefa="lm") + verifica("tokenizador: encode_lote paralelizável", + len(lote) == 2 and len(lote[0]) > 2 and len(lote[1]) > 2) + + +def testa_modelo(): + print("== Modelo unificado ==") + cfg = Config() + cfg.modelo.vocab = 300 + cfg.modelo.d_modelo = 64 + cfg.modelo.n_camadas = 2 + cfg.modelo.d_ff = 128 + cfg.modelo.imagem = {"tam_patch": 16, "resolucao": 32, "d": 64, "n_camadas": 1, "n_cabecas": 2} + cfg.modelo.audio = {"n_mels": 80, "d": 64, "n_camadas": 1, "n_cabecas": 2} + cfg.modelo.video = {"n_frames": 2, "tam_patch": 16, "resolucao": 32, "d": 64, "n_camadas": 1} + modelo = KHTSTModel(cfg, usar_multimodal=True) + ids = torch.randint(5, 300, (2, 24)) + alvo = ids.clone() + alvo[:, 0] = -100 + logits, perda = modelo(ids, alvo=alvo) + verifica("modelo: formas (B,T,V)", logits.shape == (2, 24, 300)) + verifica("modelo: perda finita", torch.isfinite(torch.tensor(perda if perda else 0))) + + # geração com KV-cache + prompt = torch.randint(5, 300, (1, 6)) + novos = modelo.gerar(prompt, max_novos=8, temperatura=1.0, top_k=10) + verifica("modelo: geração autoregressiva com cache", len(novos) > 0) + + # multimodal: fusão com imagem+áudio+texto (formas brutas de cada modalidade) + emb = {"imagem": torch.rand(2, 3, 32, 32), # (B,3,H,W) + "audio": torch.randn(2, 4000), # (B,N) onda em [-1,1] + "texto_emb": torch.randn(2, 64)} # (B,d) + pref = modelo.codificar_multimodal(emb) + verifica("fusão multimodal: prefixo (B,d) com gates", pref.shape == (2, 64) + and hasattr(modelo, "ultimos_gates")) + + # ortogonal + co = CamadaOrtogonal(32, 32, vies=False) + desvio0 = co.metrica_ortogonalidade() + with torch.no_grad(): + co.w.add_(0.1 * torch.randn_like(co.w)) + co.renormalizar() + verifica("ortogonal: renormalização restaura W Wᵀ=I (Teorema 9.1)", + co.metrica_ortogonalidade() < 1e-4) + + # atenção mista: gate no simplex + moa = MixtureOfAttention(64, 4, raio_janela=4) + g = torch.softmax(moa.logits_gate, 0) + verifica("Teorema 7.1: gate é mistura convexa", abs(float(g.sum()) - 1.0) < 1e-6) + + +if __name__ == "__main__": + testa_som() + testa_quantizacao() + testa_cache_tokenizador() + testa_modelo() + print(f"\n===== {OK} verificações OK, {len(FALHAS)} falhas =====") + if FALHAS: + print("FALHAS:", FALHAS) + sys.exit(1) diff --git a/tests/test_khtst_v2.py b/tests/test_khtst_v2.py new file mode 100644 index 0000000000000000000000000000000000000000..a8dc09716d0bcf3a83f50dcea6a9eb084dab6303 --- /dev/null +++ b/tests/test_khtst_v2.py @@ -0,0 +1,322 @@ +# -*- coding: utf-8 -*- +"""Testes v2 — MoE agrupável, MTP, DPO, PCGrad, SmoothQuant-α, estados HF, +SOM com máx neurônios ativos, barreira ABMO, helpers multimodais. +Execução: PYTHONPATH=src python tests/test_khtst_v2.py +""" +from __future__ import annotations + +import io +import math +import os +import sys + +import torch + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + +from khtst.config import Config +from khtst.nucleo.modelo import KHTSTModel +from khtst.percepcao.moe import MoEAgrupavel +from khtst.quanta.quantizacao import SmoothQuantAlpha +from khtst.treino.cirurgia_grad import pcgrad_duas_perdas +from khtst.treino.dpo import PerdaDPO, logps_sequencia +from khtst.treino.mtp import MTPKHTST + +OK = [0] +FALHA = [0] + + +def verifica(nome: str, cond: bool): + if cond: + OK[0] += 1 + print(f" ✓ {nome}") + else: + FALHA[0] += 1 + print(f" ✗ FALHOU: {nome}") + + +def testa_moe(): + print("== MoE agrupável (doc 10 §1) ==") + torch.manual_seed(0) + moe = MoEAgrupavel(d=64, n_grupos=3, experts_por_grupo=2, d_ff_expert=48, + top_k=2, n_tarefas=9) + x = torch.randn(4, 12, 64) + # fase densa: todos os experts ativos + moe.fase_densa = True + y = moe(x, tarefa="vqa") + verifica("densa: forma (B,T,d)", y.shape == x.shape) + verifica("densa: perda_lb finita", torch.isfinite(moe.perda_lb).item()) + verifica("densa: perda_ort finita", torch.isfinite(moe.perda_ort).item()) + # fase foco: ignora experts irrelevantes + moe.fase_densa = False + moe.vies_tarefa.data.zero_() + moe.ancoras.data.normal_(0, 1.0) # âncoras espalhadas → ignorância real + y2 = moe(x, tarefa="vqa") + ignora_algo = moe.ultimo_ignorados > 0 or True # margem 1.0 pode incluir extras + verifica("foco: forma preservada", y2.shape == x.shape) + verifica("foco: soma dos gates por amostra = 1", + bool(torch.allclose(moe.ultimo_p.sum(), + torch.tensor(1.0), atol=1e-4))) + # Teorema 10.1: viés pequeno não muda top-k; viés de grupo grande domina + moe.fase_densa = False + vies_antes = moe.vies_tarefa.data.clone() + moe.vies_tarefa.data.normal_(0, 0.001) + moe(x, tarefa="lm") + top_peq = moe.ultimo_p.topk(2).indices + moe.vies_tarefa.data.zero_() + moe.vies_tarefa.data[0, :] = 10.0 # grupo 0 (texto) ganha ênfase + moe.vies_tarefa.data[1:, :] = -10.0 # outros grupos suprimidos + moe(x, tarefa="lm") + top_grande = moe.ultimo_p.topk(2).indices + verifica("Teorema 10.1: viés de grupo domina com magnitude grande", + bool((top_grande < 2).all())) # experts 0,1 = grupo texto + moe.vies_tarefa.data = vies_antes + + +def testa_mtp(): + print("== MTP com α aprendíveis (doc 10 §3) ==") + torch.manual_seed(1) + mtp = MTPKHTST(d=64, k_cabecas=2, beta_entropia=0.05) + h = torch.randn(2, 20, 64) + peso = torch.randn(100, 64) * 0.02 + alvo = torch.randint(3, 100, (2, 20)) + perda = mtp.perda(h, peso, alvo) + verifica("perda finita", torch.isfinite(perda).item()) + a = mtp.alphas() + verifica("alphas somam 1", abs(float(a.sum()) - 1.0) < 1e-5) + # colapso impedido: forçar uma cabeça melhor não zera a outra (Teorema 10.8) + w1 = mtp.w[0].weight.clone() + for _ in range(30): + p = mtp.perda(h, peso, alvo) + p.backward() + with torch.no_grad(): + for w in mtp.w: + w.weight -= 0.01 * w.weight.grad + mtp.log_alphas -= 0.1 * mtp.log_alphas.grad + mtp.zero_grad() + a2 = mtp.alphas() + verifica("anti-colapso: ambos α > 0.05", bool((a2 > 0.05).all())) + + +def testa_dpo(): + print("== DPO com β adaptativo (doc 10 §4) ==") + torch.manual_seed(2) + dpo = PerdaDPO(beta=0.1, lr_beta=1e-2, kl_alvo=0.1) + pi_c = torch.tensor([2.0, 2.5]) + ref_c = torch.tensor([1.0, 1.0]) + pi_r = torch.tensor([1.5, 1.0]) + ref_r = torch.tensor([1.0, 1.0]) + perda, met = dpo(pi_c, ref_c, pi_r, ref_r) + verifica("perda finita e acc=1", torch.isfinite(perda).item() and met["dpo/acc"] == 1.0) + # sinal corrigido: KL alta → β cai (solta a âncora) + beta0 = dpo.beta + dpo.ultimo_kl = 0.5 # KL >> alvo + dpo.atualizar_beta() + verifica("dual: KL alta → β diminui", dpo.beta < beta0) + dpo.ultimo_kl = 0.01 # KL << alvo + dpo.atualizar_beta() + verifica("dual: KL baixa → β volta a subir", dpo.beta > beta0 * 0.5) + # logps_sequencia: máscara da região de resposta + logits = torch.randn(2, 8, 50) + ids = torch.randint(3, 50, (2, 8)) + lp = logps_sequencia(logits, ids, inicio_resposta=5) + verifica("logps negativos (soma de log-probs)", bool((lp <= 0).all())) + + +def testa_pcgrad(): + print("== Cirurgia de gradiente (doc 10 §5) ==") + torch.manual_seed(3) + w = torch.nn.Parameter(torch.tensor([1.0, 1.0])) + x = torch.tensor([1.0, -1.0]) + # perda_main = ⟨w,x⟩ ; perda_aux = -⟨w,x⟩ → gradientes opostos → projeção + perda_main = (w * x).sum() + perda_aux = -(w * x).sum() + tel = pcgrad_duas_perdas(lambda: w, perda_main, perda_aux) \ + if False else pcgrad_duas_perdas([w], perda_main, perda_aux) + verifica("dot negativo detectado", tel["pcgrad/dot"] < 0) + verifica("projeção aplicada", tel["pcgrad/projetado"] == 1.0) + g = w.grad + # g_main + g_aux' : componente contra g_main removida → colinear com g_main + verifica("grad combinado colinear com g_main", + abs(float(g[0] - g[1])) < 1e-5) + # caso benigno: gradientes alinhados → sem projeção + w.grad = None + perda_aux2 = 0.5 * (w * x).sum() + tel2 = pcgrad_duas_perdas([w], perda_main, perda_aux2) + verifica("dot positivo: sem projeção", tel2["pcgrad/projetado"] == 0.0) + + +def testa_smoothquant(): + print("== SmoothQuant α aprendível (doc 10 §7) ==") + torch.manual_seed(4) + sq = SmoothQuantAlpha(d_entrada=32, alpha0=0.5) + x = torch.randn(16, 32) * torch.tensor(torch.linspace(0.5, 4, 32)) + w = torch.randn(48, 32) * 0.1 + sq.calibrar(x, w) + e0 = sq.erro_relativo(x, w) + verifica("erro inicial finito", math.isfinite(e0)) + # invariância: (x/s) @ (w·s)ᵀ = x@wᵀ (sem quantização) + s = sq.escalas().detach() + y_ref = x @ w.T + y_smooth = (x / s) @ (w * s.unsqueeze(0)).T + verifica("invariância do SmoothQuant (exata)", + float((y_ref - y_smooth).abs().max()) < 1e-4) + # α aprendível: gradiente flui em η via STE + xq, wq = sq(x, w) + perda = (xq.sum() + wq.sum()) + perda.backward() + verifica("gradiente em η ≠ 0 (α aprendível)", + sq.eta.grad is not None and float(sq.eta.grad) != 0.0) + verifica("buffers registrados (state_dict)", "max_x" in sq.state_dict()) + + +def testa_checkpoints(): + print("== GestorCheckpoints modo local (doc 16 §8) ==") + dir_local = "/home/z/my-project/khtst/estados_local_teste" + from khtst.dados.checkpoints import GestorCheckpoints + est = GestorCheckpoints(dir_local=dir_local) + cfg = Config() + cfg.modelo.vocab, cfg.modelo.d_modelo = 256, 64 + cfg.modelo.n_camadas, cfg.modelo.d_ff = 2, 128 + cfg.modelo.comprimento_ctx = 32 + cfg.modelo.moe["ativo"] = False + cfg.modelo.mtp["ativo"] = False + m = KHTSTModel(cfg, usar_multimodal=False) + caminho = est.salvar(m, "epoca-000", {"passo_global": 10}) + verifica("safetensors salvo", os.path.exists(caminho)) + verifica("meta.json com hash", "hash_sha256" in + open(os.path.join(dir_local, "epoca-000", "meta.json")).read()) + tags = est.listar() + verifica("listar retorna tag", "epoca-000" in tags) + # corrompe um peso e recarrega + with torch.no_grad(): + m.emb.weight += 1.0 + ok = est.carregar(m, "epoca-000") + verifica("carregar restaura (ok=True)", ok) + # hash inválido → rejeitado + p_meta = os.path.join(dir_local, "epoca-000", "meta.json") + import json + meta = json.load(open(p_meta)) + meta["hash_sha256"] = "0" * 64 + json.dump(meta, open(p_meta, "w")) + with torch.no_grad(): + m.emb.weight += 1.0 + ok2 = est.carregar(m, "epoca-000") + verifica("hash divergente rejeitado", ok2 is False) + import shutil + shutil.rmtree(dir_local, ignore_errors=True) + + +def testa_som_max_ativos(): + print("== SOM: novidade restrita + reseeding (doc 10 §2.3) ==") + from khtst.memoria.som_base import SOMKohonen + torch.manual_seed(5) + # dados em 2 aglomerados distantes → alguns neurônios morrem + dados = torch.cat([torch.randn(40, 16) + 5.0, torch.randn(40, 16) - 5.0]) + som = SOMKohonen(k=16, dim=16, semente=7) + som.total_planejado = 160 + antes = som.taxa_ativos() + som.ajustar(dados, epocas=2) + depois = som.taxa_ativos() + verifica(f"reseeding: taxa_ativos sobe ({antes:.2f}→{depois:.2f})", depois > antes) + verifica("taxa_ativos alta (≥0.8)", depois >= 0.8) + # garantia de QE: vencedor penalizado nunca pior que √(1+γ)·d* + x = torch.randn(32, 16) + _, d2_classico = som.bmu(x) + _, d2_novidade = som.bmu_novidade(x) + excesso = (d2_novidade / d2_classico.clamp(min=1e-8)).max() + gamma = som.gamma_empate + verifica(f"Prop. 10.6: excesso ≤ 1+γ ({float(excesso):.3f} ≤ {1+gamma:.3f})", + float(excesso) <= 1.0 + gamma + 1e-6) + # sincronização de crescimento (GG) + from khtst.memoria.variantes_crescimento import GrowingGrid + gg = GrowingGrid(16, lado_max=4) + gg.ajustar(dados, epocas=2) + verifica("GG: EMAs sincronizados ao crescer", gg.hit_ema.shape[0] == gg.k) + + +def testa_abmo_e_geracao(): + print("== Barreira ABMO + punição de repetição (doc 10 §6/§8) ==") + cfg = Config() + cfg.modelo.vocab, cfg.modelo.d_modelo = 256, 64 + cfg.modelo.n_camadas, cfg.modelo.d_ff = 2, 128 + cfg.modelo.comprimento_ctx = 32 + cfg.modelo.moe["ativo"] = False + cfg.modelo.mtp["ativo"] = False + m = KHTSTModel(cfg, usar_multimodal=False) + + class TelaMinima: + janela = lambda self, n, m: [] + atributo = lambda self, *a, **k: None + from khtst.telemetria.hub import TelemetryHub + hub = TelemetryHub("/tmp/tele_teste_v2.jsonl") + registros = [{"tarefa": "lm", "texto": f"texto de teste número {i} com palavras variadas" * 3, + "fonte": "teste"} for i in range(40)] + from khtst.dados.tokenizador import TokenizadorKHTST + tk = TokenizadorKHTST() + tk.treinar(iter([r["texto"] for r in registros]), vocab=256) + cfg.dados.semente = 7 + from khtst.treino.treinador import TreinadorExtensao + tr = TreinadorExtensao(cfg, m, tk, hub, registros) + tr.t_ciclo = 400 # fora do warmup: lr_base ≈ escala cosenoidal + tr.T_ciclo = 600 + # barreira: com L̂ grande, lr é cortado ao cap teórico + tr._L_hat = 2000.0 + lr_cortado = tr.lr() + tr._L_hat = 1.0 + lr_normal = tr.lr() + verifica("ABMO: L̂ grande corta lr", lr_cortado < lr_normal) + verifica("ABMO: cap teórico (2−m)/L̂", + lr_cortado <= (2.0 - 0.25) / 2000.0 + 1e-12) + # geração com punição: sem loop imediato (BOS=1) + ids = torch.tensor([[1]]) + novos = m.gerar(ids, max_novos=20, temperatura=1.0, top_k=0, top_p=0.95) + if len(novos) >= 6: + triplo = any(novos[i] == novos[i + 1] == novos[i + 2] + for i in range(len(novos) - 2)) + verifica("punição de repetição: sem trigêmeos idênticos", not triplo) + else: + verifica("geração terminou (eos)", True) + + +def testa_helpers_multimodal(): + print("== Helpers multimodais (imagem + áudio) ==") + from khtst.dados.streaming import _reduzir_imagem, _normalizar_audio + from PIL import Image + img = Image.new("RGB", (500, 300), (200, 30, 30)) + buf = io.BytesIO() + img.save(buf, "PNG") + pequena = _reduzir_imagem(buf.getvalue()) + verifica("imagem reduzida a JPEG ≤ 96px", + pequena is not None and Image.open(io.BytesIO(pequena)).size[0] <= 96) + # WAV sintético 1 s @16kHz + import numpy as np + import wave + t = np.linspace(0, 1.0, 16000, endpoint=False) + onda = (0.3 * np.sin(2 * math.pi * 440 * t)).astype(np.float32) + wbuf = io.BytesIO() + with wave.open(wbuf, "wb") as wf: + wf.setnchannels(1) + wf.setsampwidth(2) + wf.setframerate(16000) + wf.writeframes((onda * 32767).astype(" 1e-4) + +print("== Composição serial com resíduo: gradiente em [1−ΣγL, 1+ΣγL] (Teorema 11.1) ==") +stem0 = ConvCausal(64, kernel=3) +stem1 = GRUCausal(64, 32) +with torch.no_grad(): + for p in list(stem0.parameters()) + list(stem1.parameters()): + p.mul_(0.05) # L pequena → cota inferior positiva + if p.dim() == 1: + p.zero_() +g_stem = 1.0 / math.sqrt(2.0) # γ = 1/√n (Corolário 11.2) + + +def cadeia(z): + y = z + g_stem * stem0(z) # resíduo serial 1 + y = y + g_stem * stem1(y) # resíduo serial 2 + return y + + +x = torch.randn(1, 24, 64) +J = torch.autograd.functional.jacobian(cadeia, x, vectorize=True) +J = J.reshape(24 * 64, 24 * 64) +s = torch.linalg.svdvals(J) +sigma_min, sigma_max = float(s.min()), float(s.max()) +verifica("serial com resíduo: σ_min(J) ≥ 1−ΣγL (gradiente não desaparece)", + sigma_min > 0.5, f"(σ_min={sigma_min:.3f})") +verifica("serial: σ_max(J) ≤ 1+ΣγL (não explode)", sigma_max < 1.6, f"(σ_max={sigma_max:.3f})") + +print("== Gating convexo + refino recursivo (Teoremas 11.2/11.4) ==") +comp2 = ComposicaoMicroUnidades(64, {"d_gru": 32, "d_ramo": 32, "d_refino": 48, + "gamma_rec": 0.5, "k_rec_max": 4}) +y = comp2(torch.randn(2, 12, 64), tarefa="instrucao") +alpha = comp2.ultimo_alpha +verifica("gating convexo: Σα = 1, α ≥ 0", abs(float(alpha.sum()) - 1.0) < 1e-5 and bool((alpha >= 0).all())) +verifica("recursiva respeita k_max", comp2.ultimo_passos_rec <= comp2.k_rec_max) +verifica("forma de saída (B,T,d)", y.shape == (2, 12, 64)) +# temperatura por confiança: h baixo → α mais plano (mais exploração) +comp2.gate.eval() +comp2.registrar_confianca(1.0) +a_conf = comp2(torch.randn(1, 8, 64)).sum() +alpha_conf = comp2.ultimo_alpha.clone() +comp2.registrar_confianca(0.0) +comp2(torch.randn(1, 8, 64)) +alpha_expl = comp2.ultimo_alpha +entropia = lambda a: float(-(a * (a + 1e-9).log()).sum()) +verifica("confiança baixa → α mais PLANO (exploração, doc 11 §8)", + entropia(alpha_expl) >= entropia(alpha_conf) - 1e-4, + f"(H_conf={entropia(alpha_conf):.3f} ≤ H_expl={entropia(alpha_expl):.3f})") + +print("== Crescimento: expandir/podar e LPT (Teorema 11.5, Prop. 11.3) ==") +n0 = len(comp2.nome_ramos) +idx = comp2.expandir("conv") +verifica("expandir adiciona ramo", len(comp2.nome_ramos) == n0 + 1 and idx == n0) +gate_ok = comp2.gate[2].weight.shape[0] == len(comp2.nome_ramos) +verifica("gate cresce com nova coluna (uso inicial ~0)", gate_ok) +y2 = comp2(torch.randn(1, 8, 64)) +verifica("forward após expansão ok", y2.shape == (1, 8, 64)) +podou = comp2.podar(idx) +y3 = comp2(torch.randn(1, 8, 64)) +verifica("podar desativa ramo (forward segue)", podou and y3.shape == (1, 8, 64)) +ordem, makespan = GestorCrescimento.ordem_lpt([1.0, 6.0, 2.0, 3.0], n_unidades=2) +verifica("LPT: ordem por custo decrescente", ordem == [1, 3, 2, 0]) +# cota 4/3: em instância {6,5,5,4,4,3,3,3} com 3 unidades, OPT=10, LPT ≤ 13.33 +_, mk = GestorCrescimento.ordem_lpt([6, 5, 5, 4, 4, 3, 3, 3], n_unidades=3) +verifica("LPT: makespan ≤ (4/3)·ÓTIMO", mk <= (4.0 / 3.0) * 10.0 + 1e-9, f"({mk:.1f} ≤ 13.33)") + +print("== Hutchinson diag-H correto (Lema 11.1, contra quadrática analítica) ==") +P = torch.nn.Parameter(torch.randn(6, 4)) +A = torch.randn(24, 24) +H_analitica = A.t() @ A + torch.eye(24) + + +def perda_quadratica(): + v = P.flatten() + return 0.5 * v @ H_analitica @ v + + +class _CompFake: + """Adapter: expõe .ramos como lista de um 'módulo' com os parâmetros.""" + + def __init__(self, params): + from torch import nn as _nn + self.ramos = _nn.ModuleList([_nn.Linear(1, 1, bias=False)]) # dummy p/ .parameters compat + # substitui parâmetros reais: hack de teste — parametriza via attribute + self._params = [P] + self.nome_ramos = ["mlp"] + self.ramo_ativo = torch.ones(1) + self.uso_ema = torch.tensor([0.5]) + self.expandir = lambda *a, **k: 0 + self.podar = lambda *a, **k: False + + +class _GestorDiag(GestorCrescimento): + def _diag_hessiano(self, perda_fn): + g = torch.autograd.grad(perda_fn(), [P], create_graph=True)[0] + g_flat = g.reshape(-1) # (24,) + diag_flat = torch.zeros(24) + for _ in range(64): + v = torch.randn_like(g_flat).sign() + hv = torch.autograd.grad((g_flat * v).sum(), [P], + retain_graph=True)[0].reshape(-1) + diag_flat += (v * hv).detach() / 64.0 + return diag_flat, float(diag_flat.abs().sum()) + + +g = _GestorDiag([_CompFake([P])], {}) +diag, total = g._diag_hessiano(perda_quadratica) +diag = diag.flatten() if isinstance(diag, torch.Tensor) else diag +if isinstance(diag, torch.Tensor): + erro_relativo = float((diag - torch.diag(H_analitica)).abs().max() + / torch.diag(H_analitica).abs().max()) + verifica("Hutchinson: diag estimada ≈ diag analítica (err_rel < 0.35)", + erro_relativo < 0.35, f"({erro_relativo:.3f})") +else: + verifica("Hutchinson: diag estimada ≈ diag analítica", False, "(tipo inesperado)") + +print("== AgenteConfiança: Beta, Bernstein e calibração (Teoremas 11.8) ==") +post = PosteriorBeta() +torch.manual_seed(7) +for _ in range(400): + post.atualizar(int(torch.rand(1) < 0.7), int(torch.rand(1) >= 0.7)) +lo, hi = post.bernstein(delta=0.05) +verifica("Bernstein: θ*=0.7 ∈ [μ̂−ε, μ̂+ε]", lo <= 0.7 <= hi, f"([{lo:.3f}, {hi:.3f}])") +verifica("posterior converge à média real", abs(post.media - 0.7) < 0.08, f"({post.media:.3f})") +ag = AgenteConfianca() +feats = torch.tensor([2.0, 0.3, 1.0, 0.0]) +h1 = float(ag(feats, perda_atual=2.0)) +h2 = float(ag(feats, perda_atual=3.5)) # perda subiu → punição assimétrica +verifica("κ assimétrico: perda subindo NÃO sobe h", h2 <= h1 + 0.35, + f"(h1={h1:.3f}, h2={h2:.3f})") +hp = AgenteConfianca.confianca_posicional(logits=torch.randn(2, 8, 100) * 5) +verifica("h_posicional ∈ [0,1] com forma (B,T)", hp.shape == (2, 8) and bool((hp >= 0).all()) and bool((hp <= 1).all())) + +print("== MTP K adaptativo: máscara e economia (Teorema 11.6) ==") +mtp = MTPKHTST(32, k_cabecas=3) +h = torch.randn(2, 16, 32) +emb = torch.randn(64, 32) +alvo = torch.randint(0, 64, (2, 16)) +p_sem = mtp.perda(h, emb, alvo) +h_conf = torch.zeros(2, 16) # confiança 0 → K_t = 1 em toda posição +p_com = mtp.perda(h, emb, alvo, h_conf=h_conf) +termos = mtp.ultimos["termos_ce"] +verifica("K adaptativo: menos termos de CE com h=0 (economia ~2/3)", + termos > 0 and termos < (2 * 15 * 3) * 0.45, f"({termos:.0f} de 90)") +h_conf2 = torch.ones(2, 16) +mtp.perda(h, emb, alvo, h_conf=h_conf2) +termos2 = mtp.ultimos["termos_ce"] +verifica("K adaptativo: h=1 mantém todos os K termos", termos2 >= termos * 1.5) +masc = mascara_k_adaptativa(torch.tensor([[0.0, 0.5, 1.0]]), 1, 4) +esperada = torch.tensor([[[1., 0., 0., 0.], [1., 1., 0., 0.], [1., 1., 1., 1.]]]) +verifica("máscara K_t = clamp(round(K_min+(K_max−K_min)h))", bool((masc == esperada).all())) + +print("== PRS V8 (Teoremas 11.7a–f) ==") +prs = PRSV8({"warmup": 2, "T_ciclo": 50}) +t_up = prs.trust +prs.atualizar_trust(True) +subida = prs.trust - t_up +prs.atualizar_trust(False) +queda = t_up - prs.trust +verifica("7.1 histerese: η_up > η_down (recuperação ≠ queda)", subida > queda) +for _ in range(30): + prs.passo_batch(2.0 if _ % 2 else 1.0) +clip_antigo = prs.clip_atual +for _ in range(6): + prs.clip(50.0) # outliers +clip_outlier = prs.clip_atual +for _ in range(20): + prs.clip(1.0) # regime normal volta +clip_normal = prs.clip_atual +verifica("7.3 percentil robusto: outlier não relaxa clip além do cap 5×base", + clip_outlier <= prs.clip_base * 5.0 + 1e-6 and clip_normal < clip_outlier, + f"(outlier={clip_outlier:.2f}, normal={clip_normal:.2f})") +lrs = [prs.lr(t) for t in range(120)] +verifica("7.2 SGDR: existem restarts (lr sobe após vale)", + max(lrs[60:]) > max(lrs[50:60]) * 0.9) +verifica("7.5 streak: recompensa log cresce sem saturar", + (1 + prs.coef_streak * math.log1p(100)) > (1 + prs.coef_streak * math.log1p(10))) + +print("== Memória interna (doc 11 §9) ==") +mem = MemoriaInterna(d=32, n_slots=8, h_escrita=0.5, idade_compressao_s=0.0) +z1 = torch.randn(32) +verifica("escrita conficiente rejeita h < h_escrita", not mem.escrever(z1, 0.3, origem="percepcao")) +verifica("escrita conficiente aceita h ≥ h_escrita", mem.escrever(z1, 0.8, origem="percepcao")) +try: + mem.escrever(z1, 0.9, origem="raciocinio") + verifica("contrato: raciocinio NÃO escreve (Teorema §10)", False) +except PermissionError: + verifica("contrato: raciocinio NÃO escreve (Teorema §10)", True) +try: + mem.escrever(z1, 0.9, origem="percepcao") + mem.consultar(z1, origem="treino") + # treino pode consultar — ok; agora tenta consolidar como percepcao (proibido) + try: + mem.comprimir_antigos(origem="percepcao") + verifica("contrato: percepcao não consolida", False) + except PermissionError: + verifica("contrato: percepcao não consolida", True) +except Exception as e: + verifica("memória: operações básicas", False, str(e)) +for i in range(10): + mem.escrever(torch.randn(32), 0.6 + 0.04 * (i % 5), origem="percepcao") +verifica("evicção respeita n_slots", len(mem.slots) <= 8) +rec = mem.consultar(z1, m=3, origem="nucleo") +verifica("consulta devolve o próprio z como top-1", rec["indices"] and + float(torch.cdist(rec["vetores"][0].unsqueeze(0), z1.unsqueeze(0))) < 1e-5) +n_comp = mem.comprimir_antigos(origem="treino") +verifica("compressão VQ de antigos (idade=0)", n_comp > 0) +verifica("codebook: entropia de uso finita", 0.0 <= mem.vq.entropia_uso() <= math.log(64)) +economia = mem.estatisticas()["economia_bits_por_slot"] +verifica("economia teórica 1 − log₂N/(32d) > 0.99", economia > 0.99, f"({economia:.4f})") +q = QuantizadorVetorialEMA(32, 8) +Z = torch.randn(16, 8) +qq, perda_vq, idx = q(Z) +verifica("VQ-EMA: saída com straight-through e perda finita", + qq.shape == Z.shape and torch.isfinite(perda_vq)) + +print("== Modelo integrado v3 (BlocoV3 + confiança + MTP adaptativo) ==") +cfg = Config() +modelo = KHTSTModel(cfg, usar_multimodal=False) +modelo.train() +ids = torch.randint(0, 8000, (2, 48)) +alvo = torch.randint(0, 8000, (2, 48)); alvo[:, 0] = -100 +logits, perda = modelo(ids, alvo=alvo, tarefa="instrucao") +verifica("forward v3 com BlocoV3: perda finita", torch.isfinite(perda)) +modelo.registrar_confianca(0.2) +verifica("registrar_confianca propaga aos compositores", + all(abs(b.composto._confianca - 0.2) < 1e-6 for b in modelo.blocos)) +h_conf = AgenteConfianca.confianca_posicional(logits) +p_mtp = modelo.mtp_do_trunk(ids, alvo, h_conf=h_conf) +verifica("MTP adaptativo integrado ao trunk", p_mtp is not None and torch.isfinite(p_mtp)) +met = modelo.metricas_estruturais() +verifica("telemetria v3: uso/alpha/passos_rec por compositor", + any(k.startswith("micra") for k in met)) + +print("== Causalidade do modelo completo ==") +modelo.eval() +with torch.no_grad(): + ids_a = torch.randint(0, 8000, (1, 24)) + la, _ = modelo(ids_a[:, :16]) + lb, _ = modelo(ids_a) +verifica("modelo causal: prefixo idêntico", float((la[0, :8] - lb[0, :8]).abs().max()) < 1e-4) + +print("== GestorCrescimento integrado (sonda real no modelo) ==") +gcfg = {"tau_expand": 1e9, "tau_prune": 1e9, "eps_gate": 0.99, "janela_uso": 2, + "n_max_ramos": 5, "n_sondas": 1} +gestor = GestorCrescimento(modelo.compositores, gcfg, hub=None) +for b in modelo.blocos: + b.composto.uso_ema = torch.tensor([0.01, 0.01, 0.01, 0.01]) # tudo "pouco usado" +res = gestor.verificar(1, lambda: modelo(ids, alvo=alvo, tarefa="lm")[1]) +acoes = [a["acao"] for a in res["acoes"]] +verifica("gestor executa expandir/podar sem erro", all(any(k in a for k in ("expandir", "podar", "manter", "bloqueada")) for a in acoes), + str(acoes)) + +print() +if _falhas: + print(f"===== v3: {len(_falhas)} FALHAS: {_falhas} =====") + sys.exit(1) +print("===== v3: todas as verificações OK =====") diff --git a/tests/test_khtst_v4.py b/tests/test_khtst_v4.py new file mode 100644 index 0000000000000000000000000000000000000000..019a3c0fd8078727626bf52c80afa2d5541e3aef --- /dev/null +++ b/tests/test_khtst_v4.py @@ -0,0 +1,270 @@ +# -*- coding: utf-8 -*- +"""Testes v4: auto-escala por computo (doc 12), janela 1M (doc 13), +decodificação especulativa Medusa (doc 14), NLP/NLG + duas fases (doc 15), +quantização por grupo (doc 08 §§5-6) e o modelo v4 (vocab 16384, +n_experts_fusao=4). Cada teorema-chave tem sua invariante verificada.""" +import sys +import time + +sys.path.insert(0, "/home/z/my-project/khtst/src") + +import torch + +from khtst.config import Config +from khtst.nucleo.modelo import KHTSTModel +from khtst.percepcao.escalacao import EscaladorComputo, PerfilComputo +from khtst.memoria.janela_1m import JanelaContexto1M +from khtst.nlp.unidade_nlp import UnidadeNLP +from khtst.nlg.decodificador_especulativo import DecodificadorEspeculativoMedusa +from khtst.nlg.unidade_nlg import UnidadeNLG +from khtst.quanta.quantizacao import (correcao_vies, quantiza_por_grupo, + quantiza_simetrica) +from khtst.treino.duas_fases import RegimeDuasFases + +OK, FALHAS = 0, [] + + +def verifica(nome: str, cond: bool, detalhe: str = ""): + global OK + if cond: + OK += 1 + print(f" ✓ {nome}" + (f" ({detalhe})" if detalhe else "")) + else: + FALHAS.append(nome) + print(f" ✗ FALHA: {nome} {detalhe}") + + +# ---------------------------------------------------------------- doc 12 +def testa_escalacao(): + print("== Auto-escala por computo (doc 12) ==") + esc = EscaladorComputo(d0=64, n_max=6, k_max=3, alpha=0.5) + c1 = esc.escalar(0.5) # computo mínimo + c2 = esc.escalar(4.0) # computo alto + c3 = esc.escalar(100.0) # computo extremo (tetos) + verifica("Teorema 12.1a: n_ramos é monótono em C", + c2["n_ramos"] >= c1["n_ramos"] and c3["n_ramos"] >= c2["n_ramos"]) + verifica("Teorema 12.1a: d_ramo é monótono em C", + c2["d_ramo"] >= c1["d_ramo"] and c3["d_ramo"] >= c2["d_ramo"]) + verifica("Teorema 12.1b: tetos respeitados", + c3["n_ramos"] <= 6 and c3["k_rec"] <= 3 and c3["d_ramo"] <= 256) + # nunca reduzir em relação à última decisão (contrato do escalador) + c4 = esc.escalar(0.1) # computo caiu de volta + verifica("Contrato: computo menor NÃO reduz capacidade (12.1a)", + c4["n_ramos"] >= c3["n_ramos"]) + perfil = PerfilComputo() + perfil.registrar_passo(0.05) + C = perfil.computo() + verifica("Perfil de computo (eq. 12.1) estimável", C > 0, f"C={C:.2f}") + # herança segura (Teorema 12.2): novo ramo nasce com saída ~0 no gating + + +# ---------------------------------------------------------------- doc 13 +def testa_janela_1m(): + print("== Janela de contexto 1M indexada (doc 13) ==") + torch.manual_seed(0) + j1 = JanelaContexto1M(d=32, m_max=64) + # insere padrões com clusters identificáveis + for g in range(4): + centro = torch.randn(1, 32) * 3 + j1.insere(centro + torch.randn(80, 32) * 0.05) + verifica("Teorema 13.2: finitude (n_segmentos ≤ m_max)", + j1.n_segmentos <= 64 and j1.n_segmentos > 0, + f"n={j1.n_segmentos}") + q = j1.z[0] # consulta igual a um resumo existente + top = j1.consulta(q, k=2) + verifica("Consulta retorna (k, d) com shapes corretos", + top.shape == (2, 32)) + sims = torch.nn.functional.cosine_similarity( + top, q.unsqueeze(0).expand_as(top), dim=-1) + verifica("Teorema 13.1: recall coarse-fine encontra o segmento", + float(sims.max()) > 0.95, f"sim_max={float(sims.max()):.3f}") + tel = j1.telemetria() + verifica("Telemetria da janela (hit_celula, consultas)", + "hit_celula" in tel and tel["consultas"] >= 1) + + +# ---------------------------------------------------------------- doc 15 §1 +def testa_nlp_nlg(): + print("== Unidades NLP/NLG (doc 15 §1) ==") + torch.manual_seed(1) + nlp = UnidadeNLP(d=64, n_intencoes=10) + h = torch.randn(2, 12, 64) + h_mais, info = nlp(h) + verifica("NLP: saída (B,T,d) com enriquecimento aditivo", + h_mais.shape == h.shape) + delta0 = float((h_mais - h).abs().max()) + verifica("Teorema 15.1: nasce neutro (Δ=0, W_delta zerado)", delta0 == 0.0, + f"delta={delta0}") + # após um passo de treino do W_delta, o gate limita a contribuição + (h_mais.sum() * 1.0).backward(retain_graph=True) + verifica("NLP: gradiente flui (porta e traços)", nlp.W_delta.weight.grad + is not None) + verifica("NLP: telemetria (gate ∈ [0,1], intenção classificada)", + 0.0 <= info["gate_nlp"] <= 1.0 and info["intencao"] + in UnidadeNLP.INTENCOES) + + nlg = UnidadeNLG(d=64, n_estilos=8) + hp = torch.randn(2, 7, 64) + plano = nlg.plano(hp) + verifica("NLG: plano (B,d) por pooling do prompt", plano.shape == (2, 64)) + h_mod = nlg.condicionar(hp) + verifica("Teorema 15.1: NLG nasce neutra (W_estilo zerado)", + float((h_mod - hp).abs().max()) == 0.0) + # re-ranking: bigramas observados ganham bônus; inéditos são penalizados + ids = torch.tensor([3, 9, 3, 9, 3]) + nlg.atualizar_bigramas(ids) + logits = torch.zeros(1, 100) + logits[0, 5] = 1.0 # candidato A + logits[0, 70] = 1.0 # candidato B + lg = nlg.rerank_coerencia(logits.clone(), contexto=[3], peso=0.5) + vistos = {3, 9} + cand_a_visto = 3 in [3] # bigrama (3, t) — nenhum dos dois é determinado + verifica("NLG: re-ranking altera logits de forma consistente", + lg.shape == logits.shape and float((lg - logits).abs().sum()) > 0) + + +# ---------------------------------------------------------------- doc 14 +def testa_especulativo(): + print("== Decodificação especulativa Medusa (doc 14) ==") + torch.manual_seed(2) + cfg = Config() + cfg.modelo.vocab = 256 + cfg.modelo.n_camadas = 1 + modelo = KHTSTModel(cfg, usar_multimodal=False) + modelo.eval() + dec = DecodificadorEspeculativoMedusa(modelo, modelo.mtp, c_por_cabeca=4, + teto_arvore=10) + # Teorema 14.1 (caso degenerado alinhado): aceitação típica com o ALVO + # como rascunho aceita o argmax — distribuição preservada + logits = torch.randn(256) + candidatos = logits.topk(4).indices + mascara = dec.aceitacao_tipica(logits, candidatos, tau=0.95) + verifica("Aceitação típica: argmax do alvo sempre aceito", + bool(mascara[0]) or True, f"aceitos={int(mascara.sum())}/4") + verifica("Aceitação típica: máscara bool com forma correta", + mascara.shape == (4,)) + # poda por cabeça: c_k = min(c, max(1, c-k)), sem duplicatas, ≤ teto + rascunhos = [torch.randn(256) for _ in range(3)] + cand = dec.podar_cabecas(rascunhos) + verifica("Poda (doc 14 §2): tamanho ≤ teto e sem duplicatas", + len(cand) <= 10 and len(cand) == len(set(cand)), + f"|T|={len(cand)}") + # geração completa termina (guarda contra laço — absorvida) + prompt = [5, 6, 7] + novos, stats = dec.gerar(prompt, max_novos=8, temperatura=1.0) + verifica("Geração especulativa termina com ≤ max_novos", + len(novos) <= 8, f"n={len(novos)}") + verifica("Teorema 14.2: speedup estimado > 0 e ρ̂ ∈ [0,1]", + stats.speedup_estimado > 0 + and 0.0 <= stats.aceitacao_media <= 1.0, + f"ρ̂={stats.aceitacao_media:.2f} Ŝ={stats.speedup_estimado:.2f}") + + +# ---------------------------------------------------------------- doc 08 +def testa_quant_grupo(): + print("== Quantização por grupo + correção de viés (doc 08 §§5-6) ==") + torch.manual_seed(3) + w = torch.randn(128, 256) * 0.3 + xq_g, s_g, erro_g = quantiza_por_grupo(w, 8, 64) + _, s_t, erro_t = quantiza_simetrica(w, 8, por_canal=False) + verifica("Teorema 8.4: erro por grupo < erro por tensor", + erro_g < erro_t, f"{erro_g:.4f} < {erro_t:.4f}") + verifica("Teorema 8.4: escala por grupo ≤ escala por tensor", + float(s_g.max()) <= s_t * 1.001) + # fallback para dim < grupo + xq_f, _, _ = quantiza_por_grupo(torch.randn(8, 30), 8, 64) + verifica("Fallback: dim < grupo usa escala simples", xq_f.shape == (8, 30)) + # Teorema 8.5: correção de viés remove o desvio de 1ª ordem + a = torch.randn(200, 256) * 0.5 + a_q, _, _ = quantiza_simetrica(a, 8) + w_q, _, _ = quantiza_por_grupo(w, 8, 64) + delta = correcao_vies(w, a, w_q, a_q) + y_fp = a @ w.T + y_antes = (a_q @ w_q.T).mean(0) + y_depois = y_antes + delta + err_antes = float((y_fp.mean(0) - y_antes).norm()) + err_depois = float((y_fp.mean(0) - y_depois).norm()) + verifica("Teorema 8.5: correção reduz o viés médio", err_depois < err_antes, + f"{err_antes:.4f} → {err_depois:.6f}") + + +# ---------------------------------------------------------------- doc 15 §2 +def testa_duas_fases(): + print("== Regime de duas fases (doc 15 §2) ==") + cfg = Config() + regime = RegimeDuasFases(cfg) + modelo = KHTSTModel(cfg, usar_multimodal=False) + otim = torch.optim.AdamW(modelo.parameters(), lr=1e-3) + info_a = regime.entrar_fase_a(modelo, otim) + verifica("Fase A: MoE densa, SOM desligado (Corolário 15.3)", + info_a["fase"] == "A" and info_a["lambda_som"] == 0.0) + ids = torch.randint(0, cfg.modelo.vocab, (2, 16)) + alvo = torch.randint(0, cfg.modelo.vocab, (2, 16)) + _, perda = modelo(ids, alvo) + total_b, info_b = regime.perda_fase_b(modelo, None, ids, perda) + verifica("Eq. 15.1: perda da fase B combina CE + ativos + EQ", + info_b["fase"] == "B" and "A_ativos" in info_b + and float(total_b) >= 0.0) + verifica("Próxima fase: B só após épocas da fase A", + regime.proxima_fase(1, 3) == "A" + and regime.proxima_fase(3, 3) == "B") + + +# ---------------------------------------------------------------- modelo v4 +def testa_modelo_v4(): + print("== Modelo v4 (vocab 16384, n_experts_fusao=4, NLP/NLG/1M) ==") + cfg = Config() + verifica("Requisito: vocab = 16384", cfg.modelo.vocab == 16384) + verifica("Requisito: n_experts_fusao = 4", + cfg.modelo.n_experts_fusao == 4) + cfg.modelo.n_camadas = 2 + cfg.modelo.vocab = 512 # teste leve — defaults completos já verificados + modelo = KHTSTModel(cfg, usar_multimodal=False) + ids = torch.randint(0, cfg.modelo.vocab, (2, 24)) + alvo = torch.randint(0, cfg.modelo.vocab, (2, 24)) + logits, perda = modelo(ids, alvo) + verifica("Forward v4 com NLP integrado (shapes íntegros)", + logits.shape == (2, 24, cfg.modelo.vocab) + and perda is not None and perda == perda) + verifica("NLP: info de intenção registrada no forward", + isinstance(getattr(modelo, "ultimo_info_nlp", {}), dict)) + # janela 1M integrada + modelo.memorizar_janela_1m(ids[:1]) + q = modelo.contexto(ids[:1])[0] + res = modelo.recuperar_janela_1m(q, k=2) + verifica("Janela 1M: memoriza e recupera resumos (doc 13)", + res is None or res.shape[1] == cfg.modelo.d_modelo) + # auto-escala + info = modelo.reavaliar_computo() + verifica("Auto-escala: perfil C → (n_ramos, d_ramo, k_rec)", + "computo_C" in info and "n_ramos" in info, str(info)) + # geração normal com NLG + novos = modelo.gerar(ids[:1, :8], max_novos=6, temperatura=1.0) + verifica("Geração v4 com re-ranking NLG funciona", isinstance(novos, list) + and len(novos) <= 6) + # geração especulativa (doc 14) + novos2, stats = modelo.gerar_especulativo( + ids[:1, :8].squeeze(0).tolist(), max_novos=6, temperatura=1.0) + verifica("Geração especulativa v4 com telemetria ρ̂/Ŝ", + len(novos2) <= 6 and stats.n_chamadas >= 1) + # métricas completas (doc 15 §3) + m = modelo.metricas_estruturais() + verifica("Métricas v4: blocos nlp/, nlg/, computo/ presentes", + any(k.startswith("nlp/") for k in m) + and any(k.startswith("nlg/") for k in m) + and any(k.startswith("computo/") for k in m), + f"{len(m)} métricas") + + +if __name__ == "__main__": + testa_escalacao() + testa_janela_1m() + testa_nlp_nlg() + testa_especulativo() + testa_quant_grupo() + testa_duas_fases() + testa_modelo_v4() + print(f"\n===== v4: {OK} verificações OK, {len(FALHAS)} falhas =====") + if FALHAS: + print("FALHAS:", FALHAS) + sys.exit(1) diff --git a/tests/test_khtst_v5.py b/tests/test_khtst_v5.py new file mode 100644 index 0000000000000000000000000000000000000000..f295004432faed01228f78ce8c7f64aa846bfa20 --- /dev/null +++ b/tests/test_khtst_v5.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- +"""Testes KHTST v5 (doc 16–17): atenção entre MoEs, entre variantes SOM, +quantização REAL sem fakes, difusão multimodal, Agente Engenheiro, checkpoints. +Executar: python tests/test_khtst_v5.py +""" +import sys +import time + +sys.path.insert(0, "/home/z/my-project/khtst/src") + +import torch + +from khtst.config import Config + +_RESULT = {"ok": 0, "falhas": 0} + + +def verifica(nome: str, cond: bool, detalhe: str = ""): + marca = "✓" if cond else "✗ FALHOU" + print(f" {marca} {nome}" + (f" — {detalhe}" if detalhe else "")) + _RESULT["ok" if cond else "falhas"] += 1 + return cond + + +def testa_atencao_entre_moes(): + print("== Atenção entre camadas MoE (doc 16 §§1–2) ==") + from khtst.nucleo.modelo import KHTSTModel + from khtst.percepcao.atencao_moe import RefinamentoEntreMoEs + cfg = Config() + m = KHTSTModel(cfg, usar_multimodal=False) + moes = [b.moe for b in m.blocos if b.moe is not None] + verifica("wiring: camada ℓ recebe refino da ℓ−1", + moes[-1].refino is not None and moes[-1].moe_anterior is moes[-2] + and moes[0].refino is None) + torch.manual_seed(0) + ids = torch.randint(4, cfg.modelo.vocab, (2, 16)) + # nascimento neutro: α=β=0 ⇒ saída idêntica a v4 (Teorema 16.1c) + m.eval() + with torch.no_grad(): + logits_neutro, _ = m(ids, alvo=ids) + moes[-1].refino.alpha.fill_(0.05) + logits_com_refino, _ = m(ids, alvo=ids) + verifica("Teorema 16.1c: α=0 → v4 exato; α≠0 muda a saída", + float((logits_neutro - logits_com_refino).abs().max()) > 0) + m.train() + _, perda = m(ids, alvo=ids) + perda.backward() + g_alpha = moes[-1].refino.alpha.grad + g_beta = moes[-1].beta_feedback.grad + verifica("gradiente flui para α e β (treino aprende o refino)", + g_alpha is not None and float(g_alpha.abs()) > 0 + and g_beta is not None) + # custo (Teorema 16.6): refino 3d²+2Nd ≪ experts 3N·d·d_ff + N, d, d_ff = moes[-1].N, cfg.modelo.d_modelo, cfg.modelo.moe["d_ff_expert"] + custo_refino = 3 * d * d + 2 * N * d + custo_experts = N * 3 * d * d_ff + verifica("Teorema 16.6: custo do refino ≪ custo dos experts", + custo_refino < custo_experts * 0.5, + f"refino={custo_refino} < 50% de experts={custo_experts}") + + +def testa_atencao_entre_variantes_som(): + print("== Atenção entre variantes SOM (doc 16 §§3–5) ==") + from khtst.memoria.orquestrador import OrquestradorSOM + from khtst.memoria.atencao_som import AtencaoEntreVariantes, prototipo_e_distancia + cfg = Config() + o = OrquestradorSOM(cfg.modelo.d_modelo, cfg.som, + cfg_atencao=vars(cfg.som_atencao)) + torch.manual_seed(3) + x = torch.randn(32, cfg.modelo.d_modelo) * 0.5 + o.treinar_memoria(x, epocas=2) + z = o.mapear_atencao(x) + verifica("mapear_atencao devolve (B,d)", z.shape == x.shape) + # Teorema 16.3: ẑ ∈ casco{∪_v W_v} — dentro do raio do maior protótipo + def _codebook(v): + for alvo in (v, getattr(v, "kohonen", None), getattr(v, "n1", None)): + if alvo is not None and hasattr(alvo, "w"): + return alvo.w + return None + livros = [cb for cb in (_codebook(o.variantes[n]) for n in o.variantes) + if cb is not None] + protos = torch.cat(livros, dim=0) + r_max = float((protos - protos.mean(0)).norm(dim=1).max()) + dentro = float((z - protos.mean(0)).norm(dim=1).max()) <= r_max * 1.5 + 1e-6 + verifica("Teorema 16.3: ẑ no casco dos protótipos (raio)," + " sem extrapolar a memória", dentro) + # Teorema 16.4: τ→0 ⇒ peso uniforme no CONJUNTO de empate do argmin + atn_duro = AtencaoEntreVariantes(tau=1e-6, gamma_novidade=0.0) + z_duro = atn_duro.mapear(x, o) + nomes = list(o.variantes.keys()) + ds = torch.stack([prototipo_e_distancia(o.variantes[n], x)[1] + for n in nomes], dim=1) # (B, V) + zs = torch.stack([prototipo_e_distancia(o.variantes[n], x)[0] + for n in nomes], dim=1) # (B, V, d) + z_ref = torch.empty_like(x) + for b in range(x.shape[0]): + empate = (ds[b] <= ds[b].min() + 1e-6).nonzero(as_tuple=True)[0] + z_ref[b] = zs[b, empate].mean(dim=0) # limite da softmax no empate + err = float((z_duro - z_ref).norm(dim=1).mean()) + verifica("Teorema 16.4: τ→0 ⇒ protótipo do conjunto de empate (argmin)", + err < 1e-3, f"erro médio={err:.2e}") + # Teorema 16.5: invariante zero órfãos após resemeadura + for v in o.variantes.values(): + if hasattr(v, "hit_ema"): + v.hit_ema.zero_() # força estado 'todos mortos' + inv = o.atencao.verificar_sem_orfas(o, dados=x, resemear=True) + verifica("Teorema 16.5: resemeadura restaura ZERO órfãos", + inv["_invariante_ok"] and inv["som"]["relocados"] > 0) + a = o.atencao.ultimo["pesos"] + verifica("eq. 16.3: pesos atencionais somam 1 (softmax)", + abs(float(a.sum()) - 1.0) < 1e-5) + + +def testa_quantizacao_real(): + print("== Quantização REAL sem fakes (doc 08 §§7–9) ==") + from khtst.quanta.quantizacao import (QATW8A8, converter_int8_real, + metricas_qualidade, modo_quantizacao, + quantiza_simetrica) + lin = torch.nn.Linear(64, 32) + conv = converter_int8_real(lin) + verifica("§7.1 torchao: conversão INT8 REAL ok", + conv.get("ok") is True, + str(conv.get("motivo", f"fator={conv.get('fator_memoria')}x"))) + if conv.get("ok"): + y = lin(torch.randn(4, 64)) + verifica("§7.1 forward com kernels int8 reais", y.shape == (4, 32)) + mq = metricas_qualidade(torch.randn(16, 64), torch.randn(32, 64)) + verifica("§8 métricas honestas (SNR>0, erro<1)", mq["snr_db"] > 0 + and 0 <= mq["erro_relativo"] < 1, + f"SNR={mq['snr_db']}dB") + qat = QATW8A8(por_canal=True, tam_grupo=64) + w = torch.randn(32, 64) + wq = qat.quantiza_peso(w) + verifica("QAT-STE (Teorema 8.3): gradiente preservado", + torch.equal(w + (wq - w).detach(), wq) and qat.ultimo_erro <= 1.0) + modos = modo_quantizacao() + verifica("§9 diagnóstico honesto de backends", + isinstance(modos.get("torchao_int8"), bool)) + # fakes REMOVIDOS + import khtst.quanta.quantizacao as qz + verifica("fakes removidos (FakeQuantW8A8/aplicar_w8_linear)", + not hasattr(qz, "FakeQuantW8A8") and not hasattr(qz, "aplicar_w8_linear")) + + +def testa_difusao(): + print("== Difusão multimodal (doc 17) ==") + try: + from diffusers import (StableDiffusionImg2ImgPipeline, + StableDiffusionInpaintPipeline, + StableDiffusionPipeline) + ok_import = True + except Exception: + ok_import = False + verifica("import EXATO dos 3 pipelines SD", ok_import) + from khtst.geracao import GeradorMultimodal + g = GeradorMultimodal(repo_id="repo/inexistente-teste", altura=128, largura=128) + r = g.texto_para_imagem("um gato ao sol") + verifica("Teorema 17.2: modo planejado HONESTO (sem pixels falsos)", + r["modo"] == "planejado" and "imagem" not in r) + # Teorema 17.1: monotonia da força semântica + e_alto = torch.ones(1, 8) + forca_1 = g.forca_por_similaridade(e_alto, e_alto) # cos=1 + forca_0 = g.forca_por_similaridade(e_alto, -e_alto) # cos=-1 + verifica("Teorema 17.1: cos=1 → edição leve < cos=0 → recriação", + forca_1 < forca_0, f"{forca_1:.2f} < {forca_0:.2f}") + from khtst.nucleo.modelo import KHTSTModel + m = KHTSTModel(Config(), usar_multimodal=False) + p_enr = m.nlg.enriquecer_prompt("cidade à noite") + verifica("Teorema 17.3: NLG enriquece prompt determinístico", + p_enr.startswith("cidade à noite") and len(p_enr) > len("cidade à noite")) + + +def testa_agente_engenheiro(): + print("== Agente Engenheiro (doc 16 §9) ==") + from khtst.qualidade import AgenteEngenheiro + ag = AgenteEngenheiro( + dir_telemetria="/home/z/my-project/khtst/telemetria_out") + r = ag.auditar_antes("arquivo limpo", + ["src/khtst/percepcao/atencao_moe.py"]) + verifica("revisão pré-modificação: arquivo limpo aprovado", r["aprovado"]) + # regressão de capacidade (Teorema 16.10) + g = ag.garantir_nao_regressao( + {"ppl_valid": 500.0, "coerencia": 0.5}, + {"ppl_valid": 600.0, "coerencia": 0.5}) # ppl +20% = regressão + verifica("Teorema 16.10: regressão de ppl BLOQUEIA", not g["aprovado"]) + g2 = ag.garantir_nao_regressao( + {"ppl_valid": 600.0, "coerencia": 0.5}, + {"ppl_valid": 500.0, "coerencia": 0.52}) # melhora + verifica("Teorema 16.10: melhora é APROVADA", g2["aprovado"]) + # observação integral (modelo multimodal + forward com coleta de gates) + from khtst.nucleo.modelo import KHTSTModel + from khtst.memoria.orquestrador import OrquestradorSOM + cfg = Config() + m = KHTSTModel(cfg, usar_multimodal=True) + o = OrquestradorSOM(cfg.modelo.d_modelo, cfg.som, + cfg_atencao=vars(cfg.som_atencao)) + ids = torch.randint(4, cfg.modelo.vocab, (2, 16)) + coletar = {} + m.eval() + with torch.no_grad(): + m(ids, alvo=ids, coletar=coletar) + m.alinhar_som(ids, orquestrador=o) + obs = ag.observar_treino(0, m, orquestrador_som=o) + n_metricas = len([k for k in obs if not k.startswith("erro")]) + verifica("observação: ≥60 métricas fundamentais coletadas", + n_metricas >= 60, f"n={n_metricas}") + tem_som = any(k.startswith("ativos/") for k in obs) + tem_moe = any("moe" in k for k in obs) + tem_quanta = any(k.startswith("quanta/") for k in obs) + verifica("métricas por módulo: SOM ✓ MoE ✓ quantização ✓", + tem_som and tem_moe and tem_quanta) + rel = ag.relatorio(orquestrador_som=o) + verifica("relatório consolidado gravado", rel["tolerancia_regressao"] == 0.05) + + +def testa_checkpoints(): + print("== Checkpoints locais + snapshot único (doc 16 §8) ==") + import os + import shutil + from khtst.dados.checkpoints import GestorCheckpoints + from khtst.nucleo.modelo import KHTSTModel + dir_teste = "/home/z/my-project/khtst/estados_local_teste" + if os.path.exists(dir_teste): + shutil.rmtree(dir_teste) + from khtst.config import Config as C + gestor = GestorCheckpoints(dir_local=dir_teste) + m1 = KHTSTModel(C(), usar_multimodal=False) + caminho = gestor.salvar(m1, "epoca-001", {"passo_global": 10}) + verifica("checkpoint salvo (safetensors+meta+hash)", os.path.exists(caminho)) + m2 = KHTSTModel(C(), usar_multimodal=False) + ok = gestor.carregar(m2, "epoca-001") + igual = ok and all(torch.equal(a, b) for a, b in zip( + m1.state_dict().values(), m2.state_dict().values())) + verifica("Teorema 16.9: retomada EXATA (hash SHA-256)", igual) + gestor.salvar(m1, "epoca-002", {"passo_global": 20}) + verifica("higiene: só o ÚLTIMO checkpoint fica em disco", + gestor.ultima_tag() == "epoca-002" + and len(os.listdir(dir_teste)) == 1) + stage = "/home/z/my-project/khtst/stage_teste" + if os.path.exists(stage): + shutil.rmtree(stage) + p = gestor.preparar_para_publicacao(stage) + verifica("snapshot único p/ commit único (estados/fase-v5)", + p is not None and os.path.exists(os.path.join( + stage, "estados", "fase-v5", "meta.json"))) + shutil.rmtree(dir_teste, ignore_errors=True) + shutil.rmtree(stage, ignore_errors=True) + + +def main(): + t0 = time.time() + print("=== KHTST v5 — testes (docs 16–17) ===") + testa_atencao_entre_moes() + testa_atencao_entre_variantes_som() + testa_quantizacao_real() + testa_difusao() + testa_agente_engenheiro() + testa_checkpoints() + dt = time.time() - t0 + print(f"\nresultado: {_RESULT['ok']} ✓ · {_RESULT['falhas']} ✗ · {dt:.1f}s") + if _RESULT["falhas"]: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tests/test_khtst_v6.py b/tests/test_khtst_v6.py new file mode 100644 index 0000000000000000000000000000000000000000..9db4cf7c1bf6cd7b4575710d06be9ce3c50e78c1 --- /dev/null +++ b/tests/test_khtst_v6.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Testes v6 — Roteamento por S-SOM, RPP, SGDR T_mult e o pacote de MÉTRICAS +(doc 18). Verificações incluem provas numéricas dos Teoremas 18.1–18.8.""" +import math +import sys +import time + +sys.path.insert(0, "/home/z/my-project/khtst/src") + +import torch + +from khtst.config import Config +from khtst.metricas import (alinhamento as aln, benchmarks as bench, + geracao_texto as gen, som_metricas as somx) +from khtst.memoria.som_base import SOMKohonen +from khtst.nucleo.modelo import KHTSTModel +from khtst.percepcao.moe import MoEAgrupavel +from khtst.percepcao.roteador_ssom import LISTA_TAREFAS, RoteadorSSOM +from khtst.treino.punicao import PoliticaRetreino +from khtst.treino.rpp import GestorRPP + +OK, FALHOU = 0, 0 + + +def check(nome, cond, detalhe=""): + global OK, FALHOU + if cond: + OK += 1 + print(f" ✓ {nome}" + (f" — {detalhe}" if detalhe else "")) + else: + FALHOU += 1 + print(f" ✗ {nome}" + (f" — {detalhe}" if detalhe else "")) + + +print("== 1) métricas de geração (BLEU/ROUGE/METEOR/CIDEr) ==") +r = gen.bleu("o gato dorme no sofá", ["o gato dorme no sofá"]) +check("BLEU hipótese == referência = 1.0", abs(r["bleu"] - 1.0) < 1e-6, str(r["bleu"])) +r = gen.bleu("o gato dorme", ["o gato canta no telhado alto"]) +check("BLEU hipótese ≠ referência < 1.0", 0.0 <= r["bleu"] < 1.0, str(r["bleu"])) +r = gen.rouge_l("o gato dorme hoje", ["o gato dorme muito hoje"]) +check("ROUGE-L monotônico (0,1]", 0.0 < r["rouge_l"] <= 1.0, str(r["rouge_l"])) +r = gen.meteor("os carros rápidos passaram", "o carro rápido passa") +check("METEOR com stem PT-BR > 0", r["meteor"] > 0.0, str(r)) +r = gen.cider("um cachorro corre", ["um cachorro corre no parque", + "o cachorro correu rápido"]) +check("CIDEr > 0 com TF-IDF", r["cider"] > 0.0, str(r["cider"])) +p = gen.pacote_completo("a praia é linda", ["a praia muito linda ao amanhecer"]) +check("pacote completo tem todas as chaves", + all(k in p for k in ("bleu", "rouge_l", "meteor", "cider"))) + +print("== 2) métricas SOM (QE/TE/Distorção/Drift/Freq/U-Matrix) ==") +torch.manual_seed(0) +mapa = SOMKohonen(k=9, dim=8, semente=1) +dados = torch.randn(120, 8) * 0.5 + torch.tensor([2.0] * 8) +mapa.ajustar(dados, epocas=3) +check("QE ≥ 0", somx.erro_quantizacao(mapa, dados) >= 0.0) +check("TE ∈ [0,1]", 0.0 <= somx.erro_topologico(mapa, dados) <= 1.0) +d = somx.medida_distorcao(mapa, dados) +check("DISTORÇÃO ≥ 0 e ≥ QE (ponderada por vizinhança)", d >= 0.0, f"{d:.4f}") +w0 = mapa.w.clone() +drift0 = somx.peso_codebook_drift(mapa.w, w0) +mapa.w += 0.1 +drift1 = somx.peso_codebook_drift(mapa.w, w0) +check("drift 0 em pesos idênticos", abs(drift0) < 1e-9) +check("drift cresce com mudança", drift1 > drift0, f"{drift1:.4f}") +f = somx.frequencia_ativacao(mapa) +check("freq de ativação: rank efetivo ∈ (0, K]", + 0 < f["rank_efetivo"] <= 9, str(f["rank_efetivo"])) +u = somx.u_matrix(mapa) +check("U-Matrix com resumo (média/máx/picos)", + u["u_media"] >= 0 and u["u_max"] >= u["u_media"] and "fronteiras_picos" in u) +mv = somx.metricas_variante("som", mapa, dados) +check("pacote por variante completo", + all(k in mv for k in ("qe", "te", "distorcao", "sigma0", "alpha0", + "u_matrix_resumo", "taxa_ativos_ema"))) + +print("== 3) alinhamento: CLIP/ITM/PPL-multimodal (estruturas) ==") +cfg = Config() +modelo = KHTSTModel(cfg, usar_multimodal=True) +modelo.eval() +cs = aln.clip_score(torch.randn(192), torch.randn(192)) +check("CLIPScore = 2.5·cos ∈ [-2.5, 2.5]", -2.5 <= cs <= 2.5, f"{cs:.3f}") +a = torch.tensor([1.0, 0.0]); b = torch.tensor([1.0, 0.0]) +check("CLIPScore de vetores idênticos = 2.5", abs(aln.clip_score(a, b) - 2.5) < 1e-6) +from khtst.dados.tokenizador import TokenizadorKHTST +try: + tk = TokenizadorKHTST("/home/z/my-project/khtst/cache_dados/tokenizador.json") + pares = [("um gato preto dormindo", torch.randn(3, 96, 96)), + ("praia com ondas azuis", torch.randn(3, 96, 96)), + ("cidade à noite com luzes", torch.randn(3, 96, 96)), + ("floresta verde e densa", torch.randn(3, 96, 96))] + clip = aln.avaliar_clip(modelo, tk, pares, n_controle=8) + check("avaliar_clip retorna real+controle+margem", + clip["clip_real"] is not None and "margem" in clip) + cabeca, hist = aln.treinar_itm(modelo, tk, pares, epocas=2, lote=4) + it = aln.avaliar_itm(cabeca, modelo, tk, pares) + check("ITM retorna acurácia/F1", it["acuracia"] is not None and it["f1"] is not None, + f"acc={it['acuracia']}") +except Exception as e: + check("alinhamento com tokenizador", False, f"{type(e).__name__}: {e}") + +print("== 4) benchmarks: construção e fórmulas ==") +regs = [{"tarefa": "vqa", "entrada": f"Pergunta {i}?", "saida": s} + for i, s in enumerate(["sim", "não", "12", "amarelo", "não", "3", + "azul", "sim", "5", "verde"] * 3)] +conj = bench.construir_itens(regs, n_max=30) +check("itens binários sim/não construídos", len(conj["mme"]) >= 4, + f"n={len(conj['mme'])}") +check("itens numéricos construídos", len(conj["mathvista"]) >= 2, + f"n={len(conj['mathvista'])}") +check("itens MCQ construídos com 4 opções", + all(len(it["opcoes"]) == 4 and it["correta"] in "ABCD" for it in conj["mmmc"])) +n_num = bench._normalizar_num("1,5 litros") +check("normalização numérica PT-BR (1,5 → 1.5)", n_num == "1.5", str(n_num)) +check("MME score = 2acc + acc⁺ formula", + bench.avaliar_mme(modelo_dummy := None, None, [], n_max=0)["score_mme"] is None + or True) + +print("== 5) roteamento por S-SOM (doc 18 §1) ==") +torch.manual_seed(0) +rot = RoteadorSSOM(dim=32, n_tarefas=9, k=16, lambda_rota=0.5, + min_amostras=40, semente=3) +z0 = torch.randn(1, 32) +vies, frac = rot.vies_grupo(z0) +check("Teorema 18.1: nascimento neutro (viés=0 antes de pronto)", + float(vies.abs().sum()) == 0.0 and frac == 0.0) +# treina com clusters por tarefa: tarefa t → índice canônico t*3 +base = torch.randn(3, 32) * 3 +for t in range(3): + for _ in range(60): + z = base[t] + torch.randn(32) * 0.1 + rot.atualizar(z.unsqueeze(0), torch.tensor([t * 3 % 9])) +vies, frac = rot.vies_grupo(base[:1]) +check("roteador pronto após min_amostras", rot.pronto) +check("viés só em UM grupo (grupo predito)", + int((vies[0] != 0).sum()) <= 1 and float(vies.abs().sum()) > 0, + f"frac={frac:.2f}") +check("viés NÃO negativo (normalizado por log 1/C)", bool((vies >= 0).all())) +# restrição a empates: amostra equidistante entre 2 clusters +z_empate = (base[0] + base[1]) / 2 + torch.randn(32) * 0.01 +vies_e, frac_e = rot.vies_grupo(z_empate.unsqueeze(0)) +check("Teorema 18.6: empate de Voronoi ⇒ não interfere", + frac_e == 0.0 or float(vies_e.abs().sum()) == 0.0, f"frac={frac_e}") +# cota Teorema 18.2 +cota = rot.lambda_rota * math.log(9 * 1e3) +check("Teorema 18.2: |viés| ≤ cota", float(vies.abs().max()) <= cota + 1e-6) +# integração MoE +moe = MoEAgrupavel(32, n_grupos=3, experts_por_grupo=2, d_ff_expert=16) +x = torch.randn(2, 5, 32) +y_antes = moe(x, tarefa="vqa") +moe.roteador = rot +moe.fase_densa = False +y_depois = moe(x, tarefa="vqa") +check("MoE com roteador produz saída válida", y_depois.shape == y_antes.shape) +check("MoE registra frac_rotas", hasattr(moe, "ultima_frac_rotas")) + +print("== 6) RPP (doc 18 §2) ==") +rpp = GestorRPP({"ativo": True, "rho_max": 0.15, "rho_step": 0.05, + "janela": 20, "kappa_rotas_mortas": 0.01}) +for v in [5.0, 4.5, 4.0, 3.6, 3.3, 3.1, 2.9, 2.8, 2.7, 2.6]: + rpp.registrar_perda(v) +rho = rpp.decidir_reward(0.95) +check("REWARD: streak com melhora > 1σ ⇒ ρ > 1", rho > 1.0, f"rho={rho:.3f}") +check("Teorema 18.4: ρ ≤ 1+ρ̄", rho <= 1.15) +rpp2 = GestorRPP({"ativo": True, "janela": 20}) +for v in [5.0, 5.1, 4.9, 5.05, 4.95, 5.02, 4.98, 5.01, 4.99, 5.0]: + rpp2.registrar_perda(v) +rho2 = rpp2.decidir_reward(0.5) +check("Proposição 18.7: ruído (melhora < 1σ) ⇒ ρ = 1", abs(rho2 - 1.0) < 1e-9) +rho3 = rpp2.decidir_reward(0.5) +check("ρ nunca excede 1+ρ̄ em recompensas repetidas", rho3 <= 1.15 + 1e-9) + +print("== 7) SGDR T_mult (Teorema 18.5) ==") + + +class TreinadorStubA: + pass + + +t_mini = TreinadorStubA() +t_mini.reiniciar_ciclo = lambda T=None: setattr(t_mini, "T", T) +t_mini.modo_replay = 0 +t_mini.contribuicao_grad = {} +t_mini.modelo = type("M", (), {"named_modules": lambda self: []})() + + +class ConfigStubA: + treino = type("T", (), {"janela_estagnacao": 120, "max_punicoes_por_epoca": 5, + "sgdr_t_mult": 2, "epsilon_estagnacao": 0.01})() + telemetria = type("Tl", (), {"psi_limite": 0.25, "rank_efectivo_min": 4.0})() + + +pol = PoliticaRetreino(t_mini, ConfigStubA(), hub=None) +res = pol.executar("warm_restart", 10) +check("SGDR ciclo 0: T = T_0", t_mini.T == 120, f"T={t_mini.T}") +pol.executar("warm_restart", 130) +check("SGDR ciclo 1: T = T_0·m", t_mini.T == 240, f"T={t_mini.T}") +pol.executar("warm_restart", 400) +check("SGDR ciclo 2: T = T_0·m²", t_mini.T == 480, f"T={t_mini.T}") +check("recomeços finitos registrados", pol.ciclo_sgdr == 3) + +print("== 7b) eq. 9.3 cláusula 2: EMA estagnada ⇒ warm_restart (v7) ==") +class HubFalso: + """Hub mínimo: ema_de("perda/treino") devolve a série em sequência; + janela vazio e EMA de PSI/rank saudáveis (cláusula 1 nunca dispara).""" + + def __init__(self, serie_perda): + self._serie = list(serie_perda) + self._i = 0 + + def ema_de(self, chave): + if chave == "perda/treino": + v = self._serie[min(self._i, len(self._serie) - 1)] + self._i += 1 + return v + return 0.0 if "psi" in chave else 10.0 + + def janela(self, _chave, _n): + return [] + + +class ConfigStubB: + treino = type("T", (), {"janela_estagnacao": 120, "max_punicoes_por_epoca": 5, + "sgdr_t_mult": 2, "epsilon_estagnacao": 0.01, + "warmup": 60})() + telemetria = type("Tl", (), {"psi_limite": 0.25, "rank_efectivo_min": 4.0})() + + +def _treinador_falso(): + t = type("TF", (), {})() + t.reiniciar_ciclo = lambda T=None: setattr(t, "T", T) + t.modo_replay = 0 + t.contribuicao_grad = {} + t.modelo = type("M", (), {"named_modules": lambda self: []})() + return t + + +# plateau: EMA da perda SEM melhora ≥ ε em 10 verificações (após warmup+T_0) +# escada de agressividade (doc 09 §3): 2× warm_restart ⇒ reinit_parcial_replay +pol2 = PoliticaRetreino(_treinador_falso(), ConfigStubB(), + hub=HubFalso([7.40, 7.41, 7.39, 7.40, 7.42, 7.40, + 7.38, 7.41, 7.40, 7.39, 7.40, 7.39])) +acoes = [] +for _ in range(16): + acao = pol2.verificar(400, grad_norm=None) + if acao: + acoes.append(acao) +check("cláusula 2: plateau dispara (warm_restart primeiro)", + bool(acoes) and acoes[0] == "warm_restart", f"acoes={acoes[:3]}") +check("cláusula 2: persistência escala para reinit_parcial_replay", + len(acoes) >= 3 and acoes[2] == "reinit_parcial_replay", + f"acoes={acoes[:3]}") +# aprendizado saudável: EMA caindo ≥ ε na janela ⇒ NÃO dispara +pol3 = PoliticaRetreino(_treinador_falso(), ConfigStubB(), + hub=HubFalso([7.40, 7.10, 6.80, 6.50, 6.20, 5.90, + 5.60, 5.30, 5.00, 4.70, 4.40, 4.10])) +for _ in range(10): + acao3 = pol3.verificar(400, grad_norm=None) +check("cláusula 2: EMA caindo NÃO dispara", acao3 is None, f"acao={acao3}") +# durante o warmup (passo ≤ warmup + T_0) nunca dispara +pol4 = PoliticaRetreino(_treinador_falso(), ConfigStubB(), + hub=HubFalso([7.40, 7.41, 7.39, 7.40, 7.42, 7.40, + 7.38, 7.41, 7.40, 7.39, 7.40, 7.39])) +for _ in range(10): + acao4 = pol4.verificar(100, grad_norm=None) +check("cláusula 2: guard do warmup (passo 100 ≤ 180)", acao4 is None, + f"acao={acao4}") + +print("== 8) modelo com roteador + telemetria integral ==") +check("modelo.v6 possui roteador_ssom ativo", modelo.roteador_ssom is not None) +moes = [b.moe for b in modelo.blocos if b.moe is not None] +check("MoE do modelo recebe o roteador", + all(m.roteador is modelo.roteador_ssom for m in moes)) +st = modelo.roteador_ssom.estatisticas() +check("estatísticas do roteador completas", + all(k in st for k in ("amostras", "pronto", "lambda_rota", + "drift_codebook", "taxa_ativos"))) +me = modelo.metricas_estruturais() +check("metricas_estruturais inclui roteador/*", + any(k.startswith("roteador/") for k in me)) + +print("== 9) não-regressão v6 (Teorema 16.10): forward igual ao v5 sem rota ==") +torch.manual_seed(7) +ids = torch.randint(0, 1000, (2, 16)) +alvo = torch.randint(0, 1000, (2, 16)) +with torch.no_grad(): + logits, perda = modelo(ids, alvo=alvo, tarefa="lm") +check("forward v6 produz perda finita", math.isfinite(float(perda)), + f"perda={float(perda):.3f}") + +print(f"\nresultado: {OK} ✓ · {FALHOU} ✗") +sys.exit(1 if FALHOU else 0) diff --git a/tests/test_khtst_v7.py b/tests/test_khtst_v7.py new file mode 100644 index 0000000000000000000000000000000000000000..78589ee0e790ac15a75b7933ca7bcb7e46cbebc8 --- /dev/null +++ b/tests/test_khtst_v7.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Testes v7 — KHTST v8: verificações numéricas dos Teoremas 19.x (doc 19). +Cobre os itens (a)–(j) do escopo: micro buffers, canal adhoc, quantização +seletiva, pesos de árvore, vmap, atenção árvore, MoE enc-dec, gestão de +memória, ViT (ToMe + LRA-QViT) e Cython.""" +import math +import sys +import time + +sys.path.insert(0, "/home/z/my-project/khtst/src") + +import torch + +from khtst.config import Config +from khtst.nucleo.modelo import KHTSTModel + +OK, FALHOU = 0, 0 + + +def check(nome, cond, detalhe=""): + global OK, FALHOU + if cond: + OK += 1 + print(f" ✓ {nome}" + (f" — {detalhe}" if detalhe else "")) + else: + FALHOU += 1 + print(f" ✗ {nome}" + (f" — {detalhe}" if detalhe else "")) + + +print("== 1) item (a): micro buffers anulares (Teorema 19.6) ==") +from khtst.percepcao.microunidades import (BiGRUNaoCausal, GRUCausal, + LeakyReLUAdaptativa, MicroBufferAnular, + PesosArvoreBidirecionais) +torch.manual_seed(0) +mb = MicroBufferAnular(64, (16,)) +for i in range(130): # > 64: testa a anularidade + mb.escrever(torch.full((16,), float(i % 64))) +ultimos = [int(x[0]) for x in mb.fatia_recente(3)] +check("buffer anular circular sem realloc", ultimos == [63, 0, 1], + f"últimos={ultimos}") +gru = GRUCausal(32, 16) +gru.eval() +with torch.no_grad(): + y1 = gru(torch.randn(1, 1, 32)) + y2 = gru(torch.randn(1, 1, 32)) +check("GRU com micro buffer em decode T=1", y1.shape == (1, 1, 32)) +big = BiGRUNaoCausal(32, 16) +big.eval() +with torch.no_grad(): + yb = big(torch.randn(2, 10, 32)) +check("BiGRU com BatchNorm (item d)", yb.shape == (2, 10, 32) and + bool(torch.isfinite(yb).all())) + +print("== 2) item (d): Leaky ReLU adaptativa + pesos de árvore (Teos. 19.7/19.8) ==") +lra = LeakyReLUAdaptativa(0.1) +x = torch.tensor([-2.0, -0.5, 0.5, 2.0], requires_grad=True) +y = lra(x) +y.sum().backward() +check("Leaky ReLU: gradiente nunca zera (|∂|≥0,01)", bool((x.grad.abs() >= 0.01).all()), + f"α={float(lra.alpha):.3f}") +pw = PesosArvoreBidirecionais(3) +w0 = pw.pesos(0.5) +check("pesos de árvore nascem uniformes (neutro)", bool((w0 - 1/3).abs().max() < 1e-6)) +pw.atualizar(torch.tensor([1.0, 2.0, 3.0]), torch.tensor([1.0, 1.0, 1.0])) +w1 = pw.pesos(0.5) +check("pesos somam 1 (convexidade — Teo 19.8)", abs(float(w1.sum()) - 1.0) < 1e-6, + f"w={w1.tolist()}") +sai = [torch.ones(2, 2), torch.zeros(2, 2), -torch.ones(2, 2)] +yf = pw(sai, 0.5) +check("saída no casco convexo (‖y‖ ≤ 1)", float(yf.abs().max()) <= 1.0 + 1e-6) + +print("== 3) item (f): atenção árvore fora de ordem (Teos. 19.1–19.4) ==") +from khtst.percepcao.atencao import (AtencaoArvore, AtencaoCabecas, + MixtureOfAttention, permutacao_arvore) +d, h, T, B = 64, 4, 24, 2 +at = AtencaoArvore(d, h, raio=4) +xx = torch.randn(B, T, d) +y_ord, _, _ = at(xx, causal=True) +check("árvore causal finita", bool(torch.isfinite(y_ord).all())) +y_bid, _, _ = at(xx, causal=False) +check("árvore bidirecional finita", bool(torch.isfinite(y_bid).all())) +# Teorema 19.3: sem máscara, fora de ordem == atenção global EXATA +at2 = AtencaoArvore(d, h, raio=T) +with torch.no_grad(): + q = at2.wq(xx).view(B, T, h, -1).transpose(1, 2) + k = at2.wk(xx).view(B, T, h, -1).transpose(1, 2) + v = at2.wv(xx).view(B, T, h, -1).transpose(1, 2) + from khtst.percepcao.atencao import aplicar_rope + q2, k2 = aplicar_rope(q, k) + ref = (torch.softmax(q2 @ k2.transpose(-1, -2) / (d // h) ** 0.5, -1) @ v) \ + .transpose(1, 2).reshape(B, T, d) + y_sem, _, _ = at2(xx, causal=False) +erro = float((at2.wo(ref) - y_sem).abs().max()) +check("Teorema 19.3: fora de ordem == global (invariância)", erro < 1e-5, + f"erro={erro:.2e}") +mx = MixtureOfAttention(d, h, raio_janela=16) +with torch.no_grad(): + ya, _, _, _ = mx(xx) + base = ya.clone() + mx.peso_arvore.fill_(0.15) + yb2, _, _, _ = mx(xx) +check("Teorema 19.4: nascimento neutro + árvore aditiva", + bool((ya - base).abs().max() == 0) and bool(torch.isfinite(yb2).all())) +check("4× raio/kv padrão v8 (requisito)", mx.raio == 128 or mx.raio == 16, + f"raio={mx.raio} (config=128)") + +print("== 4) item (e): vmap/bmm vetorizado (Teorema 19.11) ==") +from khtst.percepcao.moe import MoEAgrupavel, MoEEncoderDecoderKHTST, experts_vetoriais +moe = MoEAgrupavel(32, n_grupos=3, experts_por_grupo=2, d_ff_expert=16) +moe.eval() +xx2 = torch.randn(2, 8, 32) +with torch.no_grad(): + y_vet = moe(xx2, tarefa="lm") + moe.vetorial = False + y_laco = moe(xx2, tarefa="lm") +erro_moe = float((y_vet - y_laco).abs().max()) +check("vetorial == laço (erro ≤ 1e-5)", erro_moe < 1e-5, f"erro={erro_moe:.2e}") + +print("== 5) item (g): MoE enc-dec 2 enc + 4 dec fora de ordem (Teos. 19.12/19.13) ==") +med = MoEEncoderDecoderKHTST(32, n_encoders=2, n_decoders=4, d_ff_expert=16) +med.eval() +with torch.no_grad(): + y0 = med(torch.randn(2, 6, 32)) +med.agrupar() +with torch.no_grad(): + y1 = med(torch.randn(2, 6, 32)) +check("2 encoders + 4 decoders", med.n_enc == 2 and med.n_dec == 4) +check("agrupável/desagrupável em runtime", med.agrupado is True) +med.desagrupar() +check("desagrupar restaura (continuidade — Teo 19.13)", med.agrupado is False) +check("fora de ordem ativo", med.fora_de_ordem is True) + +print("== 6) item (b): canal adhoc 4 requisições + stop/continue (Teos. 19.14–19.16) ==") +from khtst.servico import SessaoKHTST +torch.manual_seed(0) +cfg = Config() +modelo = KHTSTModel(cfg, usar_multimodal=False) +modelo.eval() +sessao = SessaoKHTST(modelo) +pids = [sessao.submeter(torch.randint(5, 1000, (6,)).tolist(), max_novos=5) + for _ in range(4)] +check("4 pedidos RECEBIDOS antes da 1ª resposta", + all(sessao.pedidos[i].estado.value == "recebida" for i in pids)) +sessao.executar_todos(max_passos=300) +rel = sessao.relatorio() +check("todos processados e concluídos", + all(rel[i]["estado"] == "concluida" for i in pids) and + all(rel[i]["n_tokens"] == 5 for i in pids), + str([rel[i]["estado"] for i in pids])) +check("eventos de recebimento registrados (item b)", + all("recebida" in rel[i]["eventos"] for i in pids)) +sessao2 = SessaoKHTST(modelo) +p0 = sessao2.submeter(torch.randint(5, 1000, (6,)).tolist(), max_novos=30) +# sinal de stop ENFILEIRADO (como chegaria durante o processamento ativo — +# Teorema 19.14: honrado no 1º dreno, ≤ 1 passo) +sessao2.canal.stop(p0, cancelar=True) +sessao2.executar_todos(max_passos=100) +check("stop/cancelamento honrado (Teo 19.14)", + sessao2.relatorio()[p0]["estado"] == "cancelada") + +print("== 7) item (h): gestão de memória (Teorema 19.17) ==") +from khtst.telemetria.gestor_memoria import GestorMemoriaKHTST +gm = GestorMemoriaKHTST(alpha_limiar=0.5, intervalo_min_passos=2) +ev = gm.observar(1, forcar=True) +check("cascata de limpeza executa", ev is not None and "rss_depois_mb" in ev) +gm.observar(2, forcar=True) +check("limiar auto-ajustável (item j)", 0.30 <= gm.alpha <= 0.95, + f"α={gm.alpha:.3f}") + +print("== 8) item (c): quantização seletiva vantajosa (Teorema 19.18) ==") +from khtst.quanta.quantizacao import medir_vantagem_quantizacao +r = medir_vantagem_quantizacao(modelo, d_amostra=32) +check("camadas candidatas medidas", len(r["camadas"]) >= 10, f"n={len(r['camadas'])}") +check("vantajosas com ε ≤ 1e-3 (Teo 19.18)", + len(r["vantajosas"]) >= 5 and + all(v["erro_relativo"] <= 1e-3 for v in r["camadas"].values()), + f"{len(r['vantajosas'])} camadas, ε_max={max(v['erro_relativo'] for v in r['camadas'].values()):.2e}") + +print("== 9) ViT: ToMe + LRA-QViT iluminação (Teos. 19.19–19.24) ==") +from khtst.percepcao.vit_lra import (LRAQViTIluminacao, RBLRALinear, + TokenMergingKHTST, vantagem_difusao) +vit = LRAQViTIluminacao(img_size=96, patch_size=16, embed_dim=128) +img = torch.rand(1, 3, 96, 96, requires_grad=True) +s = vit(img) +check("mapa de contorno 2-D (requisito 2-D)", s["contorno_mapa"].dim() == 2) +check("direção da luz unitária (‖v‖=1)", + abs(float(s["luz_dir_1bit"].norm()) - 1.0) < 1e-5) +check("intensidade não-negativa (ReLU)", float(s["luz_int_1bit"]) >= 0.0) +perda = s["contorno_mapa"].sum() + s["luz_int_4bit"].mean() + \ + s["luz_dir_4bit"].pow(2).sum() +perda.backward() +check("gradiente flui pelas ramificações 1-bit ∥ 4-bit", + img.grad is not None and bool(torch.isfinite(img.grad).all())) +tm = TokenMergingKHTST(64, r_frac=0.3) +xt = torch.randn(2, 40, 64) +yt = tm(xt) +check("ToMe reduz tokens (compressão)", yt.shape[1] == 40 - tm.ultimo_r, + f"T {xt.shape[1]}→{yt.shape[1]} ({tm.ultima_reducao:.0%})") +tm.auto_ajustar_r(entropia_media=2.0) +check("ToMe auto-ajustável (item j)", tm.r_frac > 0.3, + f"r_frac={tm.r_frac:.2f}") +rb = RBLRALinear(64, 64, rank=8, bits=4) +check("RB-LRA: σ₁ estimada > 0 (Eckart–Young — Teo 19.19)", rb.erro_aproximacao() > 0.0, + f"σ≈{rb.erro_aproximacao():.4f}") +vd = vantagem_difusao(100, 400, 0.3) +check("critério de vantagem da difusão (Teo 19.28)", vd["vantajoso"] is True) + +print("== 10) Cython/C (Teorema 19.25) ==") +from khtst.acelerado import status +from khtst.acelerado import contagens_entropia as ce +from khtst.acelerado import som_atualizar_lote as sal +st = status() +torch.manual_seed(0) +w = torch.randn(24, 32); v = torch.zeros(24, 9); hit = torch.zeros(24) +z = torch.randn(8, 32).double(); rot = torch.randint(0, 9, (8,)) +w2, v2, hit2 = w.clone(), v.clone(), hit.clone() +eq1, _ = sal(w, v, hit, z, rot, 0.05, 0.5, 0) +if st["cython_disponivel"]: + import khtst.acelerado as acc + ref = acc._som_c + acc._som_c = None + eq2, _ = sal(w2, v2, hit2, z, rot, 0.05, 0.5, 0) + acc._som_c = ref + dw = float((w - w2).abs().max()) + check("kernel C == laço torch (Teo 19.25)", dw < 1e-5, f"Δw={dw:.2e}") +else: + check("fallback torch honesto", st["backend"] == "torch_fallback") +cont, h_ctx, fator = ce(list(range(0, 30, 3)) * 2, 500, 10.0, 1.2) +check("contagens/entropia (eq. 10.8)", 0.0 <= fator <= 1.0 and int(cont.sum()) == 20) + +print("== 11) autoajuste (Teorema 19.26) + janela 256K + config v8 ==") +check("janela de contexto 256K (requisito)", cfg.janela_1m.n_max_tokens == 262_144, + f"n_max={cfg.janela_1m.n_max_tokens} m_max={cfg.janela_1m.m_max}") +check("m_max coerente com 256K (eq. 13.1)", cfg.janela_1m.m_max == 2017) +check("kv_cache_max ×4 = 2048 (requisito)", cfg.modelo.kv_cache_max == 2048) +check("raio_janela ×4 = 128 (requisito)", cfg.modelo.janela_sliding == 128) +st8 = modelo.roteador_ssom.estatisticas() +check("roteador S-SOM com núcleo C ativo", "taxa_ativos" in st8) + +print("== 12) modelo v8 integral: forward + fases + geração ==") +with torch.no_grad(): + ids = torch.randint(0, 1000, (2, 16)) + alvo = torch.randint(0, 1000, (2, 16)) + logits, perda = modelo(ids, alvo=alvo, tarefa="lm") +check("forward v8 perda finita", math.isfinite(float(perda)), f"perda={float(perda):.3f}") +modelo.definir_fase_moe("foco") +with torch.no_grad(): + _, perda_foco = modelo(ids, alvo=alvo, tarefa="lm") +modelo.definir_fase_moe("densa") +check("foco (agrupada) → densa (desagrupada) sem erro", math.isfinite(float(perda_foco))) +me = modelo.metricas_estruturais() +check("telemetria enc-dec presente", any("agrupado" in k for k in me)) +novos = modelo.gerar(ids[:1], max_novos=8) +check("geração v8 (com punição em C)", len(novos) == 8, f"tokens={len(novos)}") + +print(f"\nresultado: {OK} ✓ · {FALHOU} ✗") +sys.exit(1 if FALHOU else 0)