V6.7: upload src/bigru_t/model/kohonen_learning_system.py (BBPE serial mode + OomGuard V7)
8793991 verified Download src/bigru_t/model/kohonen_learning_system.py from PowerMachine/BiGRU_T_version: direct link, hf CLI and curl.
- Browser
- Download file 228 kB
-
https://huggingface.co/PowerMachine/BiGRU_T_version/resolve/main/src/bigru_t/model/kohonen_learning_system.py
- Command line
-
hf download hf://PowerMachine/BiGRU_T_version/src/bigru_t/model/kohonen_learning_system.py
-
curl -L -o kohonen_learning_system.py https://huggingface.co/PowerMachine/BiGRU_T_version/resolve/main/src/bigru_t/model/kohonen_learning_system.py
228 kB
| """kohonen_learning_system.py — V6.5 (VQ-VAE-2 + reasoning_engine integrados) | |
| V6.5 — ATIVAR VQ-VAE-2 NO PIPELINE DE COMPRESSÃO + INTEGRAR REASONING_ENGINE | |
| Implementação CANÔNICA do Sistema de Aprendizado Kohonen 4D fornecida pelo | |
| usuário, com análise matemática formal e 2 correções de bugs críticos. | |
| ============================================================================ | |
| ANÁLISE MATEMÁTICA FORMAL | |
| ============================================================================ | |
| 1) text_to_4d_vector(text, ..., time_step, T_max): | |
| ----------------------------------------------- | |
| Tokenização BBPE: ids = BBPE_encode(text, max_len=seq_len) → [seq_len] | |
| Embedding: E = Embedding(ids) → [seq_len, D] | |
| PE sinusoidal: PE = sin/cos(pos · exp(-2k·log(10000)/D)) | |
| Fusão: F = E + PE → [seq_len, D] | |
| Centralização: F_c = F - mean(F, dim=0) → [seq_len, D] | |
| SVD: F_c = U · S · V^T (V: [D, D] ortogonal) | |
| Projeção 3D: coords_3d = F_c · V[:3,:]^T → [seq_len, 3] | |
| Centróide: xyz = mean(coords_3d, dim=0) → [3] | |
| Coordenada temporal: w = time_step / T_max ∈ [0, 1] (LINEAR) | |
| Saída: vec_4d = [x, y, z, w] → [4] | |
| Nota (V6.4): w é LINEAR no tempo (diferente de sigmoid(||xyz||)). | |
| Isto permite que o SOM organize neurônios ao longo do eixo temporal, | |
| capturando a sequência absoluta de amostras — útil para deteção | |
| de drift e consolidação incremental (EWC). | |
| 2) KohonenSOM4D: | |
| -------------- | |
| Grid: W ∈ ℝ^(I×J×K×L×4), default (6,6,6,4) = 864 neurônios | |
| BMU: bmu = argmin_{i,j,k,l} ||W[i,j,k,l] - x||² (Euclidiana ℝ⁴) | |
| Vizinhança (Gaussiana 4D): | |
| Λ(d, σ) = exp(-d² / (2σ²)) | |
| d² = Δi² + Δj² + Δk² + Δl² (distância quadrada no grid 4D) | |
| Update (Kohonen): | |
| ΔW = α · Λ · (x - W) (competitivo + cooperativo) | |
| Decaimento: | |
| σ_t = σ₀ · exp(-t/1000) (vizinhança encolhe rápido) | |
| α_t = α₀ · exp(-t/2000) (LR decai mais lento) | |
| EWC (apenas na 4ª dimensão w): | |
| L_ewc = (λ/2) · Σ F_i · (W_w,i - W*_w,i)² | |
| ∂L_ewc/∂W_w = λ · F · (W_w - W*_w) | |
| Aplicado como: update[..., 3] -= λ · F · (W_w - W*_w) | |
| Fisher (aproximação como erro quadrado): | |
| F_i = mean((x_w - W_w,i)²) sobre samples pré-punição | |
| Acumulado apenas quando: | |
| (a) punishment_count == 0 | |
| (b) old_weights_w is None (ainda não consolidado) | |
| (c) Λ > 0.1 (neurônios próximos ao BMU) | |
| 3) HypothesisClassifier: | |
| ---------------------- | |
| 8 camadas FC: [in → 512 → 256 → 128 → 64 → 32 → 16 → 8] + ReLU | |
| Output: 8 → 1 (logit) | |
| Loss: BCEWithLogitsLoss | |
| Optimizer: Adam, lr=0.001 | |
| Epochs: 50 (sobre o buffer atual) | |
| Input: vetor de ativação SOM = distâncias ao grid flatten | |
| (864-dim para grid (6,6,6,4)) | |
| 4) Punishment Protocol: | |
| --------------------- | |
| Histograma: bucketiza vec_4d[dim_choice] (default 'y', idx=1) | |
| check_training_start: | |
| max(histogram.values()) >= N_start → ready | |
| Avaliação: acc = correct / len(buffer) (correct = pred==label) | |
| Se acc < 1.0: | |
| punishment_count += 1 | |
| success_count = 0 | |
| Se punishment_count == 1: activate_hypothesis() (treina classifier) | |
| Se punishment_count == 2: | |
| set_ewc_reference() (consolida w via Fisher) | |
| required_new_samples = success_count * N (ou N se success==0) | |
| reset: training_ready=False, punishment=0, success=0, | |
| histogram cleared, buffers cleared | |
| Se acc == 1.0: | |
| punishment_count = 0 | |
| success_count += 1 | |
| 5) PGVector NÃO É MAIS NECESSÁRIO (V6.4): | |
| --------------------------------------- | |
| O método find_bmu realiza a busca nearest-neighbor sobre o grid 4D, | |
| substituindo qualquer lookup pgvector externo. O SOM interno já | |
| armazena todo o conhecimento como pesos 4D, e find_bmu retorna o | |
| neurônio mais próximo em O(I·J·K·L) — equivalente a uma busca | |
| pgvector com indexação flat. | |
| Consequentemente, hyp_t.py NÃO consulta mais pgvector — a decisão | |
| de aplicar punição é delegada ao KohonenLearningSystem. | |
| ============================================================================ | |
| BUGS CORRIGIDOS (V6.3 → mantidos em V6.4) | |
| ============================================================================ | |
| BUG 1 (find_bmu): V6.3 corrigiu return prematuro com Ellipsis. | |
| V6.4: código do usuário já está limpo (unravel manual sem return | |
| prematuro). Mantido as-is. | |
| BUG 2 (activate_hypothesis): backward tentava retropropagar através | |
| do embedding (via buffer_4d). Causava RuntimeError "Trying to | |
| backward through the graph a second time". | |
| FIX V6.4: detach+clone nos tensores de entrada do classifier, | |
| e cálculo do vetor de ativação SOM dentro de torch.no_grad(). | |
| O classifier treina apenas sobre seus próprios pesos (8 FC layers). | |
| ============================================================================ | |
| V6.5 — VQ-VAE-2 NO PIPELINE DE COMPRESSÃO | |
| ============================================================================ | |
| User requirement: "ativar efetivamente o VQ-VAE-2 no pipeline de compressão" | |
| Integração: o KohonenLearningSystem agora possui um `vqvae2_compressor` | |
| opcional (HierarchicalVQVAE2 do módulo vqvae2_hierarchical_flexnet.py). | |
| Quando ativado: | |
| 1. Após `train_som_on_buffer()`, o buffer_4d (B, 4) é passado ao VQ-VAE-2 | |
| como entrada. O encoder mapeia (B, 4) -> z_e (B, code_dim). | |
| 2. O VQ hierárquico produz: | |
| - z_q_top: estrutura global do batch (codebook K_top) | |
| - z_q_bot: detalhes residuais (codebook K_bot) | |
| 3. O decoder reconstrói z_recon (B, 4) a partir de z_q_combined. | |
| 4. A loss do VQ-VAE-2 (commitment + recon) é computada e retornada | |
| para monitoramento (não adicionada à loss do SOM — são objetivos | |
| ortogonais: SOM aprende topologia, VQ-VAE-2 aprende compressão). | |
| 5. Códigos top/bottom podem ser usados como representação compacta | |
| do estado do SOM para armazenamento/transferência. | |
| Benefícios: | |
| - Compressão neural do espaço 4D do SOM (4 -> code_dim -> 2 códigos) | |
| - Codebook compartilhado entre batches (aprendizado incremental) | |
| - Dead code restart evita colapso do codebook | |
| - Goose VQ (Gumbel-softmax) força uso uniforme do codebook | |
| ============================================================================ | |
| V6.5 — REASONING_ENGINE INTEGRADO | |
| ============================================================================ | |
| User requirement: "integrar reasoning_engine ao KohonenLearningSystem" | |
| Integração: o KohonenLearningSystem agora possui um `reasoning_engine` | |
| opcional (ReasoningEngine do módulo reasoning_engine.py). Quando ativado: | |
| 1. Após `predict()`, o resultado da predição é passado ao reasoning_engine | |
| que gera uma sequência de tags <think>, <plan>, <decompose>, | |
| <execute>, <monitor>, <predict>, <adjust>, <answer>. | |
| 2. O reasoning_engine pode usar ferramentas registradas (tool_agent) | |
| para consultas externas (ex: calculator, knowledge_base). | |
| 3. O streaming de raciocínio é compatível com Ollama/LangChain/vLLM | |
| via tags padrão. | |
| 4. Para predições do SOM, o reasoning_engine pode explicar o porquê | |
| do BMU ter sido escolhido (análise de distâncias). | |
| Métodos adicionados: | |
| - reason_about(query): retorna generator com streaming de raciocínio | |
| - reason_sync(query): retorna string completa com todas as tags | |
| - get_reasoning_stats(): retorna estatísticas do reasoning_engine | |
| ============================================================================ | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import math | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from collections import defaultdict, Counter | |
| from typing import List, Tuple, Optional, Dict, Any, Iterator | |
| # ============================================================================ | |
| # Configuração da CPU – núcleos físicos | |
| # ============================================================================ | |
| try: | |
| import psutil | |
| N_CORES = psutil.cpu_count(logical=False) | |
| except ImportError: | |
| N_CORES = os.cpu_count() // 2 if os.cpu_count() else 4 | |
| if N_CORES: | |
| try: | |
| torch.set_num_threads(N_CORES) | |
| torch.set_num_interop_threads(N_CORES) | |
| except RuntimeError: | |
| pass # already initialized | |
| # ============================================================================ | |
| # Tokenizador — DELEGADO ao módulo canônico src/bigru_t/tokenizer/bbpe_tokenizer.py | |
| # ============================================================================ | |
| # User requirement: "na subpasta 'src/bigru_t/tokenizer' está o tokenizer | |
| # bbpe_tokenizer.py que deve ser usado portanto REMOVER tokenizadores | |
| # implementados em outros módulos". | |
| # | |
| # V7-tokenizer-cleanup: a classe SimpleBBPETokenizer local foi REMOVIDA | |
| # COMPLETAMENTE. Não há mais fallback word-level duplicado nesta classe — | |
| # o BBPETokenizer canônico JÁ possui seu próprio fallback word-level interno | |
| # (`_wordlevel_fallback`) e OOM-safe serial training (`_train_serial_inprocess`). | |
| # Manter um segundo fallback aqui era duplicação de código e confundia a | |
| # auditoria (qual tokenizer está realmente ativo?). | |
| # | |
| # SimpleBBPETokenizer agora é APENAS um alias direto para BBPETokenizer. | |
| # Imports existentes (ex: `from bigru_t.model.kohonen_learning_system import | |
| # SimpleBBPETokenizer`) continuam funcionando, mas não há mais nenhuma | |
| # implementação de tokenizer neste arquivo. | |
| try: | |
| from bigru_t.tokenizer.bbpe_tokenizer import BBPETokenizer as SimpleBBPETokenizer | |
| _HAS_CANONICAL_TOKENIZER = True | |
| except Exception as _tok_err: | |
| _HAS_CANONICAL_TOKENIZER = False | |
| # Fallback final: define um stub mínimo para que o import não quebre | |
| # (apenas para testes isolados sem o pacote bigru_t instalado). | |
| # Em produção, o pacote bigru_t está sempre instalado. | |
| class SimpleBBPETokenizer: # type: ignore[no-redef] | |
| """Stub fallback — NÃO é uma implementação de tokenizer. | |
| Apenas evita ImportError quando bigru_t.tokenizer não está disponível. | |
| Em produção, use BBPETokenizer de src/bigru_t/tokenizer/bbpe_tokenizer.py. | |
| """ | |
| def __init__(self, vocab_size: int = 16384): | |
| self.vocab_size = vocab_size | |
| raise RuntimeError( | |
| "BBPETokenizer canônico não disponível. Instale o pacote bigru_t " | |
| "ou adicione src/ ao PYTHONPATH." | |
| ) | |
| # ============================================================================ | |
| # Codificação posicional e conversão texto → vetor 4D (com w temporal) | |
| # ============================================================================ | |
| def positional_encoding(seq_len: int, hidden_dim: int) -> torch.Tensor: | |
| """PE sinusoidal clássico: PE[pos, 2k] = sin(pos·exp(-2k·log(10000)/D)), | |
| PE[pos, 2k+1] = cos(pos·exp(-2k·log(10000)/D)). | |
| """ | |
| pe = torch.zeros(seq_len, hidden_dim) | |
| position = torch.arange(0, seq_len, dtype=torch.float).unsqueeze(1) | |
| div_term = torch.exp( | |
| torch.arange(0, hidden_dim, 2).float() * (-math.log(10000.0) / hidden_dim) | |
| ) | |
| pe[:, 0::2] = torch.sin(position * div_term) | |
| pe[:, 1::2] = torch.cos(position * div_term) | |
| return pe | |
| def text_to_4d_vector( | |
| text: str, | |
| tokenizer: SimpleBBPETokenizer, | |
| embedding: nn.Embedding, | |
| hidden_dim: int, | |
| seq_len: int, | |
| time_step: int, | |
| T_max: int = 10000, | |
| attention: Optional[nn.Module] = None, | |
| attention_metrics: Optional[Dict[str, Any]] = None, | |
| ) -> torch.Tensor: | |
| """Converte sentença em vetor 4D (x, y, z, w), onde w = time_step / T_max. | |
| Pipeline matemático (V6.5-attn: + MultiHeadAttention entre +PE e SVD): | |
| ids → Embedding → +PE → [MultiHeadAttention] → SVD(proj top-3) → centróide xyz | |
| w = time_step / T_max (LINEAR no tempo) | |
| vec_4d = concat(xyz, w) → [4] | |
| V6.5-attn (user requirement: "verificar se o mecanismo de atenção está ativo | |
| e acessado logicamente funcional"): | |
| Quando `attention` é fornecido (nn.Module), o tensor `fused (L, D)` é | |
| passado pelo attention (self-attention com máscara causal) antes do SVD. | |
| O attention produz um tensor residual (fused + attn_out) preservando a | |
| dimensão. Métricas são coletadas em `attention_metrics` para verificação. | |
| Args: | |
| attention: Optional[nn.Module] — MultiHeadAttention do attention_multimodal. | |
| attention_metrics: Optional[Dict] — acumula estatísticas do attention. | |
| Se fornecido, é atualizado in-place com: n_calls, last_norm_in, | |
| last_norm_out, last_attn_activated (True se output != input). | |
| """ | |
| ids = tokenizer.encode(text, max_length=seq_len) | |
| input_ids = torch.tensor(ids).unsqueeze(0) | |
| word_emb = embedding(input_ids).squeeze(0) # (L, D) | |
| pe = positional_encoding(seq_len, hidden_dim) | |
| fused = word_emb + pe # (L, D) | |
| # V6.5-attn — Aplica MultiHeadAttention (self-attention) sobre o fused | |
| # User requirement: "verificar se o mecanismo de atenção está ativo e | |
| # acessado logicamente funcional" | |
| if attention is not None: | |
| try: | |
| # Prepare input: (1, L, D) — batch dim necessária para MHA | |
| fused_in = fused.unsqueeze(0) # (1, L, D) | |
| norm_in = float(fused_in.norm().item()) | |
| # Forward — passa mask=None (sem causal mask; seq_len curto) | |
| attn_out = attention(fused_in, mask=None) # (1, L, D) | |
| # Residual connection (preserva dimensão) | |
| fused_attn = fused_in + attn_out # (1, L, D) | |
| fused = fused_attn.squeeze(0) # (L, D) | |
| norm_out = float(fused.norm().item()) | |
| # Métricas de verificação (in-place se dict fornecido) | |
| if attention_metrics is not None: | |
| attention_metrics["n_calls"] = attention_metrics.get("n_calls", 0) + 1 | |
| attention_metrics["last_norm_in"] = norm_in | |
| attention_metrics["last_norm_out"] = norm_out | |
| # "logicamente funcional" = output é finito e diferente do input | |
| attention_metrics["last_attn_activated"] = bool( | |
| torch.isfinite(attn_out).all().item() | |
| and (attn_out.abs().sum().item() > 1e-8) | |
| ) | |
| attention_metrics["last_attn_diff_norm"] = float( | |
| (attn_out.squeeze(0) - fused_in.squeeze(0)).norm().item() | |
| ) | |
| except Exception as _e_attn: | |
| # Em caso de erro no attention, segue sem attention mas registra | |
| if attention_metrics is not None: | |
| attention_metrics["n_errors"] = attention_metrics.get("n_errors", 0) + 1 | |
| attention_metrics["last_error"] = str(_e_attn)[:200] | |
| # SVD para 3D | |
| mean_centered = fused - fused.mean(dim=0, keepdim=True) | |
| # V6.5-V2-metrics-FIX-3 — SVD com fallback robusto: se a matriz for | |
| # rank-deficiente (ex: todos os tokens são <pad> → todas as linhas | |
| # idênticas), torch.linalg.svd pode produzir NaN ou levantar RuntimeError. | |
| # Fallback 1: driver 'gesvd' (mais estável para matrizes singulares). | |
| # Fallback 2: zeros (vetor 4D neutro, não contamina o SOM). | |
| try: | |
| U, S, V = torch.linalg.svd(mean_centered, full_matrices=False) | |
| except Exception: | |
| try: | |
| U, S, V = torch.linalg.svd(mean_centered, full_matrices=False, driver='gesvd') | |
| except Exception: | |
| S = torch.zeros(min(mean_centered.shape), device=mean_centered.device) | |
| V = torch.eye(mean_centered.shape[1], device=mean_centered.device) | |
| # V6.5-V2-metrics-FIX-3 — sanitiza NaN do SVD (rank-deficient → S pode ter NaN) | |
| if torch.isnan(S).any() or torch.isinf(S).any(): | |
| S = torch.nan_to_num(S, nan=0.0, posinf=0.0, neginf=0.0) | |
| V = torch.nan_to_num(V, nan=0.0, posinf=0.0, neginf=0.0) | |
| coords_3d = torch.mm(mean_centered, V[:3, :].t()) # (L, 3) | |
| xyz_mean = coords_3d.mean(dim=0) # (3,) | |
| w = torch.tensor(time_step / T_max, dtype=torch.float) # valor temporal | |
| # V6.5-V2-auto-conscience-v2 — Normalização SEPARADA de xyz e w. | |
| # ANTES: vetores 4D tinham magnitude ~1e-4 (xyz) e ~1e-4 (w). Após L2 norm, | |
| # w dominava (≈1.0) e xyz virava ~1e-3. Todos os vetores apontavam para | |
| # [0,0,0,1] com pequenas perturbações, fazendo SOM mapear tudo para o | |
| # mesmo neurônio (neurons_active=1/864). | |
| # | |
| # AGORA: normalizamos xyz e w INDEPENDENTEMENTE para [-1, 1]: | |
| # - xyz: L2-normalizado (preserva direção semântica do texto) | |
| # - w: mapeado para [-1, 1] via (2w - 1) = 2·(t/T_max) - 1 | |
| # Isto garante que ambas as componentes têm peso comparável no SOM, | |
| # permitindo que diferentes textos mapeiem para diferentes neurônios. | |
| xyz_norm = xyz_mean.norm() | |
| if xyz_norm > 1e-8: | |
| xyz_normalized = xyz_mean / xyz_norm # unit vector in xyz subspace | |
| else: | |
| # Texto degenerado — usa direção aleatória reproducível baseada em time_step | |
| torch.manual_seed(int(time_step) % 1000) | |
| xyz_normalized = torch.randn(3) | |
| xyz_normalized = xyz_normalized / xyz_normalized.norm() | |
| # w mapeado de [0, 1] para [-1, 1] (melhor distribuição no espaço 4D) | |
| w_normalized = 2.0 * w - 1.0 | |
| vec_4d = torch.cat([xyz_normalized, w_normalized.unsqueeze(0)]).contiguous() | |
| return vec_4d | |
| # ============================================================================ | |
| # Mapa de Kohonen 4D com EWC (Fisher para w não-nulo) | |
| # ============================================================================ | |
| class KohonenSOM4D: | |
| """Mapa Auto-Organizável 4D com EWC apenas na 4ª dimensão (w temporal). | |
| Args: | |
| grid_shape: (I, J, K, L) — dimensões do grid 4D. | |
| alpha0: taxa de aprendizado inicial (α₀). | |
| sigma0: largura inicial da vizinhança (σ₀). | |
| lambda_ewc: peso da penalidade EWC (λ). | |
| Atributos: | |
| weights: W ∈ ℝ^(I×J×K×L×4) — pesos dos neurônios. | |
| old_weights_w: W*_w ∈ ℝ^(I×J×K×L) — referência EWC (apenas w). | |
| fisher_w: F ∈ ℝ^(I×J×K×L) — informação de Fisher por neurônio (w). | |
| fisher_accum / fisher_count: acumuladores para cálculo de F. | |
| """ | |
| def __init__( | |
| self, | |
| grid_shape: Tuple[int, int, int, int], | |
| alpha0: float = 0.5, | |
| sigma0: float = 3.0, | |
| lambda_ewc: float = 0.01, | |
| # V6.5-V2-auto-conscience-v2 — initial γ raised from 0.1 to 0.5 to | |
| # ensure strong conscience bias from the start (prevents 2/864 bug). | |
| conscience_gamma: float = 0.5, | |
| conscience_beta: float = 0.1, | |
| ): | |
| # V6.5-V2-metrics-FIX-3 — α₀=0.5 e σ₀=3.0 conforme especificação | |
| # canônica para SOM 4D (Kohonen classic): | |
| # - α₀ ∈ [0.5, 1.0] para fase de ordenação (rough training) | |
| # - σ₀ = metade da maior dimensão da grade = max(6,6,6,4)/2 = 3.0 | |
| # V6.5-V2-metrics-FIX-3 — floors explícitos no update_weights garantem | |
| # que α_t e σ_t nunca decaiam abaixo de 0.001 e 0.1 respectivamente | |
| # (sem isso, após ~7000 updates σ→0 e o SOM degenera em k-means puro, | |
| # perdendo preservação topológica). | |
| # | |
| # V6.5-V2-metrics-FIX-4 (Conscience Mechanism — DeSieno 1988): | |
| # Corrige o problema neurons_active=2/864 relatado pelo usuário. | |
| # Cada neurônio i mantém uma frequência de vitória p_i ∈ [0,1]. | |
| # BMU selection: bmu = argmin_i ( ||W_i - x||² - b_i ) | |
| # onde b_i = γ · (1/N - p_i) é o bias de consciência | |
| # γ = 0.1 (default), N = total de neurônios (864) | |
| # Update da frequência (EMA): p_i ← (1-β)·p_i + β·𝟙[i==bmu], β=0.1 | |
| # Quando um neurônio ganha demais (p_i > 1/N), b_i fica negativo | |
| # (penaliza), e quando nunca ganha (p_i ≈ 0), b_i fica positivo | |
| # (empurra para ganhar). Isto força distribuição uniforme de BMU. | |
| self.I, self.J, self.K, self.L = grid_shape | |
| self.alpha0 = float(alpha0) | |
| self.sigma0 = float(sigma0) | |
| self.lambda_ewc = lambda_ewc | |
| self.t = 0 | |
| # V6.5-V2-auto-conscience-v2 — Inicialização diversa para evitar | |
| # neurônios com pesos idênticos (que causaria todos os inputs a | |
| # mapearem para o mesmo BMU). Usa grid coordinates como offset | |
| # inicial + ruído aleatório, garantindo que cada neurônio comece | |
| # em uma região diferente do espaço 4D. | |
| # User requirement: "distribuindo o processamento paralelamente". | |
| # Cada uma das 4 dimensões do peso recebe uma coordenada de grid | |
| # diferente (i, j, k, l), normalizada para [-1, 1]. | |
| grid_i = torch.arange(self.I).float().view(self.I, 1, 1, 1) / max(1, self.I - 1) | |
| grid_j = torch.arange(self.J).float().view(1, self.J, 1, 1) / max(1, self.J - 1) | |
| grid_k = torch.arange(self.K).float().view(1, 1, self.K, 1) / max(1, self.K - 1) | |
| grid_l = torch.arange(self.L).float().view(1, 1, 1, self.L) / max(1, self.L - 1) | |
| # Broadcast para (I, J, K, L) — cada coordenada ocupa seu próprio eixo | |
| grid_bc = grid_i * grid_j * grid_k * grid_l # (I, J, K, L) com valores ∈ [0,1] | |
| # Repete para 4 dimensões de peso, aplicando offset diferente para cada | |
| # dim para que neurônios vizinhos tenham pesos diferentes. | |
| w_dim_0 = (grid_i.expand(self.I, self.J, self.K, self.L) - 0.5) * 2.0 # [-1,1] em I | |
| w_dim_1 = (grid_j.expand(self.I, self.J, self.K, self.L) - 0.5) * 2.0 # [-1,1] em J | |
| w_dim_2 = (grid_k.expand(self.I, self.J, self.K, self.L) - 0.5) * 2.0 # [-1,1] em K | |
| w_dim_3 = (grid_l.expand(self.I, self.J, self.K, self.L) - 0.5) * 2.0 # [-1,1] em L | |
| # Stack na última dimensão → (I, J, K, L, 4) | |
| grid_coords = torch.stack([w_dim_0, w_dim_1, w_dim_2, w_dim_3], dim=-1) | |
| # Combina grid coords (determinístico) com ruído (estocástico) | |
| self.weights = grid_coords + 0.3 * torch.randn(self.I, self.J, self.K, self.L, 4) | |
| self.old_weights_w = None | |
| self.fisher_w = None | |
| self.fisher_accum = torch.zeros(self.I, self.J, self.K, self.L) | |
| self.fisher_count = torch.zeros(self.I, self.J, self.K, self.L) | |
| # V6.6 — Fisher accumulation CONTÍNUA (online EWC pattern) | |
| # Quando True, Fisher é acumulado em TODOS os updates (não só | |
| # antes da primeira consolidação). Após set_ewc_reference(), | |
| # Fisher é resetado e começa a acumular novamente para a próxima | |
| # tarefa. A consolidada é preservada via EMA em finalize_fisher(). | |
| self.fisher_continuous: bool = True | |
| # Histórico de Fisher consolidations (uma entrada por set_ewc_reference) | |
| self.fisher_consolidation_count: int = 0 | |
| # V6.5-V2-metrics-FIX-4 — Conscience mechanism (DeSieno 1988) | |
| # Inicializa p_i = 1/N (uniforme) — sem viés inicial. | |
| # b_i = γ · (1/N - p_i) começa em 0 (sem bias). | |
| self.n_neurons = self.I * self.J * self.K * self.L | |
| self.conscience_gamma = float(conscience_gamma) | |
| self.conscience_beta = float(conscience_beta) | |
| target_p = 1.0 / self.n_neurons | |
| self.win_frequency = torch.full( | |
| (self.I, self.J, self.K, self.L), target_p, dtype=torch.float | |
| ) | |
| # Contador absoluto de vitórias por neurônio (para auditoria) | |
| self.bmu_win_count = torch.zeros( | |
| self.I, self.J, self.K, self.L, dtype=torch.long | |
| ) | |
| # Histórico dos últimos K BMUs (para revive_dead_neurons) | |
| self._recent_bmu_flat: List[int] = [] | |
| # ------------------------------------------------------------------ | |
| # V6.5-V2-auto-conscience — Estado para ajuste adaptativo de γ. | |
| # User requirement: "aprimorar conscience_gamma para ajuste | |
| # automático conforme a quantidade de neurônios esteja caindo". | |
| # | |
| # Math (DeSieno 1988 + adaptação adaptativa): | |
| # r = n_active / n_total (∈ [0,1]) | |
| # γ_base = γ_max · (1 - r)^2 (intervenção cresce | |
| # com colapso) | |
| # drop = max(0, r_prev - r) (tendência de queda) | |
| # γ_boost = 0.3 · drop (reforço antecipado) | |
| # γ_target = clip(γ_base + γ_boost, γ_min, γ_max) | |
| # γ_new = (1 - η) · γ_old + η · γ_target (EMA suave) | |
| # | |
| # Comportamento esperado: | |
| # r = 1.0 → γ_base = 0.00 (data distribution domina) | |
| # r = 0.5 → γ_base = 0.25 (intervenção moderada) | |
| # r = 0.1 → γ_base = 0.81 (intervenção forte) | |
| # r = 0.0 → γ_base = 1.00 (intervenção máxima) | |
| # r caindo 1.0→0.5 → +0.15 boost (antecipa colapso) | |
| # ------------------------------------------------------------------ | |
| self._conscience_gamma_min = 0.01 | |
| self._conscience_gamma_max = 1.0 | |
| self._conscience_eta = 0.05 # EMA smoothing (normal regime) | |
| self._prev_n_active = -1 | |
| self._prev_n_active_rate = -1.0 | |
| self._last_gamma_target = float(conscience_gamma) | |
| self._last_gamma_drop = 0.0 | |
| self._last_activation_rate = 1.0 | |
| # V6.5-V2-auto-conscience-v2 — reduzido de 50 → 10 para detectar | |
| # colapso de neurônios mais cedo e ajustar γ a tempo. | |
| self._auto_adjust_interval = 10 # ajusta γ a cada 10 updates | |
| # V6.5-V2-kmeans-pp — flag para inicialização k-means++ deferida. | |
| # User requirement: "inicialização com k-means++ sobre embeddings em | |
| # vez de grid coords+ruído dos embeddings que saem do tokenizador | |
| # para ambas as FASE1 e FASE2". Como no momento do __init__ ainda | |
| # não temos dados, a init k-means++ é DEFERIDA: ela acontece na | |
| # primeira chamada de init_weights_kmeans_pp(data_buffer) feita | |
| # pelo KLS quando o buffer_4d tem >= 64 amostras. | |
| self._kmeans_pp_initialized = False | |
| self._kmeans_pp_min_samples = 64 | |
| # ====================================================================== | |
| # V6.5-V2-kmeans-pp — Inicialização k-means++ sobre embeddings | |
| # User requirement: "inicialização com k-means++ sobre embeddings em | |
| # vez de grid coords+ruído dos embeddings que saem do tokenizador | |
| # para ambas as FASE1 e FASE2". | |
| # Math (Arthur & Vassilvitskii 2007): | |
| # 1. c_1 = data[índice aleatório] | |
| # 2. Para c_2..c_N: escolhe c_i = data[j] com prob ∝ D(j)^2 | |
| # onde D(j) = min_k ||x_j - c_k||^2 | |
| # 3. Após seeding, roda n_iter iterações de Lloyd para refinar centros. | |
| # Vantagem: distribui pesos nas regiões de ALTA densidade dos dados, | |
| # reduzindo dead neuron rate inicial. | |
| # ====================================================================== | |
| def init_weights_kmeans_pp( | |
| self, | |
| data_buffer, | |
| n_iter: int = 5, | |
| random_seed=None, | |
| ): | |
| """Inicializa pesos do SOM via k-means++ (Arthur & Vassilvitskii 2007). | |
| Args: | |
| data_buffer: lista de tensores [4] — amostras do buffer_4d do KLS. | |
| n_iter: número de iterações de Lloyd (default 5). | |
| random_seed: seed para reprodutibilidade. | |
| Returns: | |
| Dict com: initialized, n_samples_used, n_neurons_assigned, | |
| inertia (soma das dist^2 ao centro mais próximo). | |
| """ | |
| if not data_buffer or len(data_buffer) < 8: | |
| return { | |
| "initialized": False, | |
| "reason": f"insufficient_data ({len(data_buffer) if data_buffer else 0} < 8)", | |
| } | |
| if random_seed is not None: | |
| g = torch.Generator().manual_seed(int(random_seed)) | |
| else: | |
| g = None | |
| with torch.no_grad(): | |
| try: | |
| data = torch.stack( | |
| [v.detach().clone().float() if isinstance(v, torch.Tensor) | |
| else torch.tensor(v, dtype=torch.float) | |
| for v in data_buffer] | |
| ).float() | |
| except Exception: | |
| return {"initialized": False, "reason": "stack_failed"} | |
| data = torch.nan_to_num(data, nan=0.0, posinf=1.0, neginf=-1.0) | |
| N = data.shape[0] | |
| P = self.n_neurons | |
| if N < P: | |
| repeats = (P // N) + 1 | |
| data = data.repeat(repeats, 1)[:P * 4] | |
| N = data.shape[0] | |
| # ---- k-means++ seeding ---- | |
| if g is not None: | |
| first_idx = int(torch.randint(0, N, (1,), generator=g).item()) | |
| else: | |
| first_idx = int(torch.randint(0, N, (1,)).item()) | |
| centers = [data[first_idx].clone()] | |
| for k_idx in range(1, P): | |
| C = torch.stack(centers) | |
| data_sq = (data * data).sum(dim=-1, keepdim=True) | |
| C_sq = (C * C).sum(dim=-1, keepdim=True).t() | |
| cross = data @ C.t() | |
| dist_sq = data_sq + C_sq - 2.0 * cross | |
| dist_sq = torch.clamp(dist_sq, min=0.0) | |
| D_sq_min = dist_sq.min(dim=-1).values | |
| D_sum = D_sq_min.sum().clamp(min=1e-12) | |
| probs = D_sq_min / D_sum | |
| if g is not None: | |
| new_idx = int(torch.multinomial(probs, 1, generator=g).item()) | |
| else: | |
| new_idx = int(torch.multinomial(probs, 1).item()) | |
| centers.append(data[new_idx].clone()) | |
| centers_tensor = torch.stack(centers) | |
| centers_tensor = centers_tensor + 0.02 * torch.randn(P, 4) | |
| # ---- Lloyd iterations (refinamento) ---- | |
| for it in range(n_iter): | |
| data_sq = (data * data).sum(dim=-1, keepdim=True) | |
| C_sq = (centers_tensor * centers_tensor).sum(dim=-1, keepdim=True).t() | |
| cross = data @ centers_tensor.t() | |
| dist_sq = data_sq + C_sq - 2.0 * cross | |
| dist_sq = torch.clamp(dist_sq, min=0.0) | |
| nearest = dist_sq.argmin(dim=-1) | |
| for p in range(P): | |
| mask = (nearest == p) | |
| if mask.any(): | |
| centers_tensor[p] = data[mask].mean(dim=0) | |
| data_sq = (data * data).sum(dim=-1, keepdim=True) | |
| C_sq = (centers_tensor * centers_tensor).sum(dim=-1, keepdim=True).t() | |
| cross = data @ centers_tensor.t() | |
| dist_sq = data_sq + C_sq - 2.0 * cross | |
| dist_sq = torch.clamp(dist_sq, min=0.0) | |
| inertia = float(dist_sq.min(dim=-1).values.sum().item()) | |
| perm = torch.randperm(P, generator=g) | |
| centers_shuffled = centers_tensor[perm] | |
| new_weights = centers_shuffled.view(self.I, self.J, self.K, self.L, 4).clone() | |
| new_weights = torch.clamp(new_weights, -10.0, 10.0) | |
| self.weights = new_weights | |
| self._kmeans_pp_initialized = True | |
| return { | |
| "initialized": True, | |
| "n_samples_used": int(N), | |
| "n_neurons_assigned": int(P), | |
| "n_lloyd_iters": int(n_iter), | |
| "inertia": float(inertia), | |
| "method": "kmeans++ (Arthur & Vassilvitskii 2007)", | |
| } | |
| def _neighborhood(self, bmu_idx): | |
| """Vizinhança Gaussiana 4D: d² = Δi² + Δj² + Δk² + Δl².""" | |
| i, j, k, l = bmu_idx | |
| II, JJ, KK, LL = torch.meshgrid( | |
| torch.arange(self.I).float(), | |
| torch.arange(self.J).float(), | |
| torch.arange(self.K).float(), | |
| torch.arange(self.L).float(), | |
| indexing="ij", | |
| ) | |
| dist_sq = (II - i) ** 2 + (JJ - j) ** 2 + (KK - k) ** 2 + (LL - l) ** 2 | |
| return dist_sq | |
| def find_bmu(self, x: torch.Tensor, use_conscience: bool = True) -> Tuple[int, int, int, int]: | |
| """Best Matching Unit: argmin ||W - x||² em ℝ⁴, com conscience bias opcional. | |
| Substitui pgvector_lookup — busca nearest-neighbor flat sobre o grid. | |
| V6.5-V2-auto-conscience-v2 (FIX math asymmetry): | |
| A fórmula anterior b_i = γ·(1/N - p_i) dava bias quase zero para | |
| neurônios mortos (γ/N ≈ 0.001 para N=864), enquanto penalizava | |
| fortemente neurônios vencedores (γ·p_i ≈ 0.3γ). Isto causava | |
| neurons_active=2/864 — a vantagem dada a neurônios mortos era | |
| insuficiente para superar a diferença natural de distância. | |
| Nova fórmula (simétrica, DeSieno 1988 corrigida): | |
| p_norm_i = bmu_win_count[i] / max(1, max(bmu_win_count)) | |
| bias_i = γ · (0.5 - p_norm_i) (simétrica em [-0.5γ, +0.5γ]) | |
| dist_eff = dist² - bias_i | |
| Comportamento: | |
| - Neurônio dominante (p_norm = 1.0): bias = -0.5γ → dist_eff = dist² + 0.5γ (PENALIZADO) | |
| - Neurônio nunca-vencedor (p_norm = 0): bias = +0.5γ → dist_eff = dist² - 0.5γ (FAVORECIDO) | |
| - Magnitudes iguais → distribuição efetiva do BMU entre os 864 neurônios. | |
| V6.5-V2-auto-conscience-v2 (forced dead-neuron exploration): | |
| Para neurônios com bmu_win_count == 0, aplica um boost adicional | |
| de -γ·0.5 (reduz distância efetiva em 0.5γ). Isto garante que | |
| neurônios mortos sejam explorados ativamente, distribuindo o | |
| processamento paralelamente (user requirement). | |
| Args: | |
| x: tensor [4] — vetor de entrada. | |
| use_conscience: se True, aplica bias de consciência (default True). | |
| """ | |
| # Sanitiza x antes de computar distância | |
| if torch.isnan(x).any() or torch.isinf(x).any(): | |
| x = torch.nan_to_num(x, nan=0.0, posinf=1e4, neginf=-1e4) | |
| dist = torch.sum((self.weights - x.view(1, 1, 1, 1, 4)) ** 2, dim=-1) | |
| if use_conscience: | |
| # V6.5-V2-auto-conscience-v2 — bias simétrico baseado em win_count normalizado | |
| with torch.no_grad(): | |
| win_count_flat = self.bmu_win_count.flatten().float() | |
| max_win = win_count_flat.max().clamp(min=1.0) | |
| p_norm = win_count_flat / max_win # ∈ [0, 1] | |
| # Bias simétrico: +0.5γ para mortos, -0.5γ para dominante | |
| bias = self.conscience_gamma * (0.5 - p_norm) | |
| # Boost adicional para neurônios nunca-vencedores (win_count == 0) | |
| dead_mask = (win_count_flat == 0).float() | |
| dead_boost = self.conscience_gamma * 0.5 * dead_mask # -0.5γ na distância | |
| # dist_effective = dist² - bias - dead_boost | |
| # (bias positivo reduz distância → favorece; bias negativo aumenta → penaliza) | |
| bias_flat = bias.flatten() + dead_boost | |
| dist_flat = dist.flatten() - bias_flat | |
| flat_idx = int(torch.argmin(dist_flat).item()) | |
| else: | |
| flat_idx = int(torch.argmin(dist).item()) | |
| i = flat_idx // (self.J * self.K * self.L) | |
| rest = flat_idx % (self.J * self.K * self.L) | |
| j = rest // (self.K * self.L) | |
| rest = rest % (self.K * self.L) | |
| k = rest // self.L | |
| l = rest % self.L | |
| return (i, j, k, l) | |
| def update_win_frequency(self, bmu_idx: Tuple[int, int, int, int]) -> None: | |
| """V6.5-V2-metrics-FIX-4 — Atualiza frequência de vitória (EMA). | |
| p_i ← (1-β)·p_i + β·𝟙[i==bmu], β = conscience_beta (default 0.1) | |
| Também mantém contador absoluto bmu_win_count para auditoria. | |
| Deve ser chamado APÓS find_bmu e APÓS update_weights. | |
| Args: | |
| bmu_idx: (i, j, k, l) — índice do BMU selecionado. | |
| """ | |
| with torch.no_grad(): | |
| # EMA update | |
| self.win_frequency = ( | |
| (1.0 - self.conscience_beta) * self.win_frequency | |
| ) | |
| self.win_frequency[bmu_idx] += self.conscience_beta | |
| # Contador absoluto | |
| self.bmu_win_count[bmu_idx] += 1 | |
| # Histórico recente (para revive_dead_neurons) | |
| flat = ( | |
| bmu_idx[0] * (self.J * self.K * self.L) | |
| + bmu_idx[1] * (self.K * self.L) | |
| + bmu_idx[2] * self.L | |
| + bmu_idx[3] | |
| ) | |
| self._recent_bmu_flat.append(flat) | |
| # Mantém últimos 200 BMUs | |
| if len(self._recent_bmu_flat) > 200: | |
| self._recent_bmu_flat = self._recent_bmu_flat[-200:] | |
| def revive_dead_neurons( | |
| self, | |
| data_buffer: Optional[List[torch.Tensor]] = None, | |
| dead_threshold: int = 0, | |
| ) -> Dict[str, Any]: | |
| """V6.5-V2-metrics-FIX-4 — Revive neurônios mortos reinicializando pesos. | |
| User requirement: "ANALISAR matematicamente e logicamente a ativação e | |
| uso e acesso dos neurônios (apenas dois estão sendo ativados: | |
| neurons_active=2/864) distribuindo o processamento paralelamente". | |
| Um neurônio é considerado "morto" se bmu_win_count <= dead_threshold | |
| (nunca ou raramente foi BMU). Para cada neurônio morto: | |
| 1. Se data_buffer fornecido: amostra um vetor aleatório do buffer e | |
| atribui aos pesos do neurônio (reinicialização data-driven). | |
| 2. Se buffer vazio: reinicializa com ruído gaussiano pequeno (N(0, 0.1)). | |
| 3. Reseta win_frequency para 1/N (sem bias) e bmu_win_count para 0. | |
| Isto garante que TODOS os 864 neurônios sejam utilizados, distribuindo | |
| o processamento paralelo do SOM conforme solicitado. | |
| Args: | |
| data_buffer: lista de tensores [4] — amostras do buffer_4d do KLS. | |
| dead_threshold: neurônios com win_count <= threshold são revividos. | |
| Returns: | |
| Dict com: n_revived, n_total, revived_indices, dead_rate_before, dead_rate_after. | |
| """ | |
| with torch.no_grad(): | |
| dead_mask = self.bmu_win_count <= dead_threshold | |
| n_dead = int(dead_mask.sum().item()) | |
| n_total = self.n_neurons | |
| dead_rate_before = float(n_dead / n_total) | |
| revived_indices: List[Tuple[int, int, int, int]] = [] | |
| if n_dead == 0: | |
| return { | |
| "n_revived": 0, | |
| "n_total": n_total, | |
| "revived_indices": [], | |
| "dead_rate_before": dead_rate_before, | |
| "dead_rate_after": dead_rate_before, | |
| "action": "none", | |
| } | |
| # Amostra pontos do buffer se disponível | |
| buffer_tensor = None | |
| if data_buffer and len(data_buffer) > 0: | |
| try: | |
| buffer_tensor = torch.stack( | |
| [v.detach().clone() if isinstance(v, torch.Tensor) | |
| else torch.tensor(v, dtype=torch.float) | |
| for v in data_buffer] | |
| ).float() | |
| except Exception: | |
| buffer_tensor = None | |
| # V6.5-V2-kmeans-pp-revive — Reinicializa neurônios mortos usando | |
| # estratégia k-means++ (max-min selection): em vez de amostrar um | |
| # ponto aleatório do buffer, escolhe o ponto do buffer que está | |
| # MAIS DISTANTE de todos os neurônios ativos atuais. Isto garante | |
| # que os pesos revividos cubram regiões do espaço de entrada que | |
| # não estavam sendo representadas, maximizando a diversidade | |
| # topológica do SOM. | |
| # User requirement: "reforçar auto_revive para reinicializar pesos | |
| # dos neurônios mortos (não só boostar γ)". | |
| dead_indices = dead_mask.nonzero(as_tuple=False) | |
| # Identifica neurônios ATIVOS (win_count > 0) para cálculo de distância | |
| active_mask = self.bmu_win_count > 0 | |
| active_indices = active_mask.nonzero(as_tuple=False) | |
| # Stack pesos ativos (centros atuais do SOM) | |
| if active_indices.shape[0] > 0: | |
| active_weights = self.weights[active_mask] # (n_active, 4) | |
| else: | |
| active_weights = None | |
| for idx_tensor in dead_indices: | |
| i, j, k, l = idx_tensor.tolist() | |
| if buffer_tensor is not None and buffer_tensor.shape[0] > 0: | |
| if active_weights is not None and active_weights.shape[0] > 0: | |
| # k-means++ selection: ponto mais distante dos ativos | |
| # dist²[j, k] = ||buffer[j] - active[k]||² | |
| buf_sq = (buffer_tensor * buffer_tensor).sum(dim=-1, keepdim=True) # (B, 1) | |
| act_sq = (active_weights * active_weights).sum(dim=-1, keepdim=True).t() # (1, A) | |
| cross = buffer_tensor @ active_weights.t() # (B, A) | |
| dist_sq = buf_sq + act_sq - 2.0 * cross | |
| dist_sq = torch.clamp(dist_sq, min=0.0) | |
| # D²(j) = min_k ||buf[j] - active[k]||² | |
| D_sq_min = dist_sq.min(dim=-1).values # (B,) | |
| # Escolhe o ponto com maior D² (mais distante) | |
| sample_idx = int(torch.argmax(D_sq_min).item()) | |
| new_w = buffer_tensor[sample_idx].clone() | |
| # Pequeno ruído para evitar duplicação exata | |
| new_w = new_w + 0.05 * torch.randn(4) | |
| # Adiciona o novo peso à lista de ativos para próxima iteração | |
| active_weights = torch.cat([active_weights, new_w.unsqueeze(0)], dim=0) | |
| else: | |
| # Sem ativos: amostra aleatória simples | |
| sample_idx = torch.randint(0, buffer_tensor.shape[0], (1,)).item() | |
| new_w = buffer_tensor[sample_idx].clone() | |
| new_w = new_w + 0.05 * torch.randn(4) | |
| # Inicia lista de ativos | |
| active_weights = new_w.unsqueeze(0).clone() | |
| else: | |
| # Reinicialização gaussiana pequena | |
| new_w = 0.1 * torch.randn(4) | |
| new_w = torch.clamp(new_w, -10.0, 10.0) | |
| self.weights[i, j, k, l] = new_w | |
| # Reset counters | |
| self.win_frequency[i, j, k, l] = 1.0 / n_total | |
| self.bmu_win_count[i, j, k, l] = 0 | |
| revived_indices.append((i, j, k, l)) | |
| # Recalcula dead rate após revival | |
| new_dead_mask = self.bmu_win_count <= dead_threshold | |
| n_dead_after = int(new_dead_mask.sum().item()) | |
| dead_rate_after = float(n_dead_after / n_total) | |
| return { | |
| "n_revived": len(revived_indices), | |
| "n_total": n_total, | |
| "revived_indices": revived_indices[:50], # top 50 para log | |
| "n_dead_before": n_dead, | |
| "n_dead_after": n_dead_after, | |
| "dead_rate_before": dead_rate_before, | |
| "dead_rate_after": dead_rate_after, | |
| "action": "revived" if revived_indices else "none", | |
| "used_buffer": buffer_tensor is not None, | |
| } | |
| def parallel_neuron_activation_report(self) -> Dict[str, Any]: | |
| """V6.5-V2-metrics-FIX-4 — Relatório estruturado de ativação dos 864 neurônios. | |
| User requirement: "distribuindo o processamento paralelamente" + o | |
| exemplo de código fornecido mostra estruturação vetorizada para | |
| análise estatística e auditoria do modelo. | |
| Este método produz um relatório análogo ao DataFrame do exemplo, | |
| mas otimizado para o grid 4D (6,6,6,4) com 864 neurônios: | |
| Returns: | |
| Dict com: | |
| - n_total_neurons: int (864) | |
| - n_active_neurons: int (vitória em ≥1 amostra histórica) | |
| - n_dead_neurons: int (nunca foi BMU) | |
| - neuron_activation_rate: float | |
| - max_win_count: int (neurônio mais ativo) | |
| - min_win_count: int (neurônio menos ativo) | |
| - mean_win_count: float | |
| - std_win_count: float | |
| - max_win_frequency: float | |
| - min_win_frequency: float | |
| - bmu_distribution_top20: dict {flat_idx: count} | |
| - bmu_distribution_bottom20: dict {flat_idx: count} (mortos) | |
| - conscience_bias_mean: float (deve tender a 0 se uniforme) | |
| - conscience_bias_std: float (deve tender a 0 se uniforme) | |
| - uniformity_score: float (1 - CV da win_frequency, ∈ [0,1]) | |
| """ | |
| with torch.no_grad(): | |
| win_counts_flat = self.bmu_win_count.flatten().float() | |
| win_freq_flat = self.win_frequency.flatten() | |
| n_total = self.n_neurons | |
| n_active = int((self.bmu_win_count > 0).sum().item()) | |
| n_dead = n_total - n_active | |
| # Estatísticas | |
| if n_total > 0: | |
| max_wc = float(win_counts_flat.max().item()) | |
| min_wc = float(win_counts_flat.min().item()) | |
| mean_wc = float(win_counts_flat.mean().item()) | |
| std_wc = float(win_counts_flat.std().item()) | |
| max_wf = float(win_freq_flat.max().item()) | |
| min_wf = float(win_freq_flat.min().item()) | |
| # Uniformidade: 1 - CV (coeficiente de variação) | |
| cv = float(std_wc / max(mean_wc, 1e-8)) | |
| uniformity = max(0.0, 1.0 - cv) | |
| else: | |
| max_wc = min_wc = mean_wc = std_wc = 0.0 | |
| max_wf = min_wf = 0.0 | |
| uniformity = 0.0 | |
| # Bias de consciência: b_i = γ · (1/N - p_i) | |
| target_p = 1.0 / n_total | |
| bias_flat = self.conscience_gamma * (target_p - win_freq_flat) | |
| bias_mean = float(bias_flat.mean().item()) | |
| bias_std = float(bias_flat.std().item()) | |
| # Top-20 BMUs mais frequentes | |
| from collections import Counter | |
| recent_counter = Counter(self._recent_bmu_flat) | |
| top20 = dict(recent_counter.most_common(20)) | |
| # Bottom-20 (mortos ou raros) — últimos em vitórias absolutas | |
| sorted_indices = torch.argsort(win_counts_flat) | |
| bottom20_idx = sorted_indices[:20].tolist() | |
| bottom20 = { | |
| int(idx): int(self.bmu_win_count.flatten()[idx].item()) | |
| for idx in bottom20_idx | |
| } | |
| return { | |
| "n_total_neurons": int(n_total), | |
| "n_active_neurons": int(n_active), | |
| "n_dead_neurons": int(n_dead), | |
| "neuron_activation_rate": float(n_active / max(1, n_total)), | |
| "max_win_count": max_wc, | |
| "min_win_count": min_wc, | |
| "mean_win_count": mean_wc, | |
| "std_win_count": std_wc, | |
| "max_win_frequency": max_wf, | |
| "min_win_frequency": min_wf, | |
| "bmu_distribution_top20": {str(k): int(v) for k, v in top20.items()}, | |
| "bmu_distribution_bottom20": {str(k): int(v) for k, v in bottom20.items()}, | |
| "conscience_bias_mean": bias_mean, | |
| "conscience_bias_std": bias_std, | |
| "uniformity_score": uniformity, | |
| "conscience_gamma": float(self.conscience_gamma), | |
| "conscience_beta": float(self.conscience_beta), | |
| # V6.5-V2-auto-conscience — telemetria do ajuste adaptativo | |
| "conscience_gamma_min": float(self._conscience_gamma_min), | |
| "conscience_gamma_max": float(self._conscience_gamma_max), | |
| "conscience_gamma_target": float(self._last_gamma_target), | |
| "conscience_gamma_drop": float(self._last_gamma_drop), | |
| "activation_rate_at_last_adjust": float(self._last_activation_rate), | |
| "prev_n_active_at_last_adjust": int(self._prev_n_active), | |
| "n_recent_bmus_tracked": int(len(self._recent_bmu_flat)), | |
| } | |
| # ================================================================== | |
| # V6.5-V2-parallel — Vectorial parallel SOM (ParallelSOM4D pattern) | |
| # User requirement: "Para elevar o desempenho ao nível máximo, a arquitetura | |
| # com multiprocessing (baseada em CPU) que usamos anteriormente não é a | |
| # ideal para cenários de alta dimensão. O custo de copiar os dados entre | |
| # múltiplos processos do Python (overhead de IPC) anula o ganho da | |
| # computação paralela. Para um ajuste paralelo verdadeiro e massivo de uma | |
| # SOM de 4 dimensões, a abordagem ideal é o paralelismo vetorial em GPU | |
| # utilizando PyTorch." | |
| # | |
| # Math: todas as operações são feitas via broadcasting PyTorch: | |
| # 1. distances = sum((flat_weights - sample)^2, dim=-1) # (N_neurons,) | |
| # 2. bmu_idx = argmin(distances) | |
| # 3. grid_dists_sq = sum((grid_coords - bmu_coord)^2, dim=-1) # (N_neurons,) | |
| # 4. influence = exp(-grid_dists_sq / (2 * sigma^2)) # (N_neurons,) | |
| # 5. weights += alpha * influence * (sample - weights) # broadcast (N, 4) | |
| # ================================================================== | |
| def _precompute_grid_coords_flat(self) -> torch.Tensor: | |
| """Pré-computa coordenadas 4D da grade planificadas (N_neurons, 4). | |
| V6.5-V2-parallel — pré-computação evita recriar meshgrid a cada update. | |
| Usado por train_batch_parallel e reorganize_map_by_density. | |
| """ | |
| if not hasattr(self, "_grid_coords_flat_cache") or self._grid_coords_flat_cache is None: | |
| II, JJ, KK, LL = torch.meshgrid( | |
| torch.arange(self.I).float(), | |
| torch.arange(self.J).float(), | |
| torch.arange(self.K).float(), | |
| torch.arange(self.L).float(), | |
| indexing="ij", | |
| ) | |
| grid_coords = torch.stack([II, JJ, KK, LL], dim=-1) # (I, J, K, L, 4) | |
| self._grid_coords_flat_cache = grid_coords.view(-1, 4) # (N, 4) | |
| return self._grid_coords_flat_cache | |
| def train_batch_parallel( | |
| self, | |
| samples: torch.Tensor, | |
| use_conscience: bool = True, | |
| accumulate_fisher: bool = False, | |
| ) -> Dict[str, Any]: | |
| """V6.5-V2-parallel — Treina SOM com paralelismo vetorial PyTorch. | |
| User requirement (ParallelSOM4D pattern): processa amostras usando | |
| operações matriciais PyTorch ao invés de loops Python. Cada amostra | |
| ainda é processada sequencialmente (online learning — Kohonen canônico), | |
| mas todas as operações internas (distância, BMU, vizinhança, update) | |
| são feitas via broadcasting sobre os 864 neurônios simultaneamente. | |
| Vantagens: | |
| 1. Zero IPC overhead (sem multiprocessing) | |
| 2. Broadcast de tensores — uma instrução matricial atualiza todos os | |
| pesos da hipergrade 4D simultaneamente | |
| 3. Compatível com AVX512_VNNI / AMX_INT8 via xeon_runtime | |
| 4. Memória eficiente: apenas 2 tensores temporários (dist, influence) | |
| Args: | |
| samples: tensor (B, 4) — batch de amostras 4D. | |
| use_conscience: se True, aplica bias de consciência (default True). | |
| accumulate_fisher: se True, acumula Fisher info em w. | |
| Returns: | |
| Dict com: n_processed, n_bmu_unique, dead_rate, alpha_t, sigma_t. | |
| """ | |
| if not isinstance(samples, torch.Tensor): | |
| samples = torch.tensor(samples, dtype=torch.float) | |
| samples = samples.float() | |
| if samples.dim() == 1: | |
| samples = samples.unsqueeze(0) | |
| # Sanitiza NaN/Inf | |
| samples = torch.nan_to_num(samples, nan=0.0, posinf=1e4, neginf=-1e4) | |
| n_processed = 0 | |
| bmu_indices: List[Tuple[int, int, int, int]] = [] | |
| grid_coords_flat = self._precompute_grid_coords_flat() | |
| with torch.no_grad(): | |
| for sample_idx in range(samples.shape[0]): | |
| x = samples[sample_idx] # (4,) | |
| # 1. PARALELISMO MATRICIAL: distância de 1 amostra vs TODOS os neurônios | |
| flat_weights = self.weights.view(-1, 4) # (N, 4) | |
| dist = torch.sum((flat_weights - x.unsqueeze(0)) ** 2, dim=-1) # (N,) | |
| # 2. BMU selection (com conscience opcional) | |
| if use_conscience: | |
| win_count_flat = self.bmu_win_count.flatten().float() | |
| max_win = win_count_flat.max().clamp(min=1.0) | |
| p_norm = win_count_flat / max_win | |
| bias = self.conscience_gamma * (0.5 - p_norm) | |
| dead_mask = (win_count_flat == 0).float() | |
| dead_boost = self.conscience_gamma * 0.5 * dead_mask | |
| bias_flat = bias + dead_boost | |
| dist_eff = dist - bias_flat | |
| flat_idx = int(torch.argmin(dist_eff).item()) | |
| else: | |
| flat_idx = int(torch.argmin(dist).item()) | |
| # Recupera coordenada 4D da BMU | |
| i = flat_idx // (self.J * self.K * self.L) | |
| rest = flat_idx % (self.J * self.K * self.L) | |
| j = rest // (self.K * self.L) | |
| rest = rest % (self.K * self.L) | |
| k = rest // self.L | |
| l = rest % self.L | |
| bmu_idx = (i, j, k, l) | |
| bmu_indices.append(bmu_idx) | |
| # 3. Vizinhança 4D paralela — distância na grade | |
| bmu_coord = grid_coords_flat[flat_idx] # (4,) | |
| grid_dists_sq = torch.sum((grid_coords_flat - bmu_coord.unsqueeze(0)) ** 2, dim=-1) # (N,) | |
| # 4. Hiperparâmetros com floors | |
| sigma = max(self.sigma0 * math.exp(-self.t / 1000), 0.1) | |
| alpha = max(self.alpha0 * math.exp(-self.t / 2000), 0.001) | |
| # 5. Influência Gaussiana | |
| influence = torch.exp(-grid_dists_sq / (2 * sigma ** 2)) # (N,) | |
| influence = influence.view(self.I, self.J, self.K, self.L, 1) # (I,J,K,L,1) | |
| # 6. Update em massa — uma operação vetorial | |
| delta = x.view(1, 1, 1, 1, 4) - self.weights # (I,J,K,L,4) | |
| update = alpha * influence * delta | |
| # EWC penalty na 4ª dimensão | |
| if self.old_weights_w is not None and self.fisher_w is not None: | |
| ewc_penalty = self.lambda_ewc * self.fisher_w * ( | |
| self.weights[..., 3] - self.old_weights_w | |
| ) | |
| update[..., 3] = update[..., 3] - ewc_penalty | |
| self.weights = self.weights + update | |
| self.weights = torch.clamp(self.weights, -100.0, 100.0) | |
| if torch.isnan(self.weights).any() or torch.isinf(self.weights).any(): | |
| self.weights = torch.nan_to_num( | |
| self.weights, nan=0.0, posinf=100.0, neginf=-100.0 | |
| ) | |
| # 7. Fisher accumulation | |
| if accumulate_fisher: | |
| influence_flat = influence.view(-1) | |
| mask = influence_flat > 0.1 | |
| if mask.any(): | |
| diff_sq = (x[3] - self.weights.view(-1, 4)[mask, 3]) ** 2 | |
| self.fisher_accum.view(-1)[mask] += diff_sq | |
| self.fisher_count.view(-1)[mask] += 1 | |
| self.t += 1 | |
| n_processed += 1 | |
| # 8. Atualiza conscience (win frequency) | |
| try: | |
| self.update_win_frequency(bmu_idx) | |
| except Exception: | |
| pass | |
| # 9. Auto-ajuste de γ (throttled) | |
| try: | |
| if self.t % self._auto_adjust_interval == 0: | |
| self.auto_adjust_conscience_gamma() | |
| except Exception: | |
| pass | |
| # Estatísticas pós-batch | |
| n_unique_bmus = len(set(bmu_indices)) | |
| n_active = int((self.bmu_win_count > 0).sum().item()) | |
| dead_rate = float(1.0 - n_active / max(1, self.n_neurons)) | |
| sigma_t = max(self.sigma0 * math.exp(-self.t / 1000), 0.1) | |
| alpha_t = max(self.alpha0 * math.exp(-self.t / 2000), 0.001) | |
| return { | |
| "n_processed": n_processed, | |
| "n_bmu_unique": n_unique_bmus, | |
| "dead_rate": dead_rate, | |
| "n_active": n_active, | |
| "n_total": self.n_neurons, | |
| "alpha_t": float(alpha_t), | |
| "sigma_t": float(sigma_t), | |
| "som_t": int(self.t), | |
| "conscience_gamma": float(self.conscience_gamma), | |
| "method": "vectorial_parallel_pytorch", | |
| } | |
| # ================================================================== | |
| # V6.5-V2-reorganize — Density-based map reorganization | |
| # (ReorganizingSOM4D pattern) | |
| # User requirement: "Para reorganizar o mapa SOM dinamicamente durante o | |
| # treinamento, a técnica matemática mais robusta e paralelizável é a | |
| # Filtragem de Ativação por Convolução Baseada em Densidade. Em vez de | |
| # alterar a topologia de forma sequencial (o que quebraria o paralelismo), | |
| # nós calculamos a frequência de ativação de cada neurônio (densidade de | |
| # uso) diretamente na GPU/CPU e aplicamos um filtro gaussiano | |
| # multidimensional. Os neurônios pouco ativados (isolados) sofrem uma força | |
| # de atração em direção aos agrupamentos de alta densidade, reorganizando | |
| # geometricamente o hipercubo 4D sem perder o alinhamento matricial." | |
| # | |
| # Math: | |
| # density_i = bmu_win_count[i] / sum(bmu_win_count) ∈ [0, 1] | |
| # density_center = sum(weights * density, dim=0) (centro de massa ponderado) | |
| # pull_force_i = (1 - density_i) * regularization_factor | |
| # new_weights_i = weights_i + pull_force_i * (density_center - weights_i) | |
| # | |
| # Comportamento: | |
| # - Neurônios com alta densidade (muito ativos): pull_force ≈ 0 → ficam parados | |
| # - Neurônios com baixa densidade (mortos): pull_force ≈ 1 → puxados para o centro | |
| # - Centro de massa é dominado pelos neurônios ativos (ponderação por densidade) | |
| # ================================================================== | |
| def reorganize_map_by_density( | |
| self, | |
| regularization_factor: float = 0.05, | |
| min_dead_rate_for_trigger: float = 0.3, | |
| ) -> Dict[str, Any]: | |
| """V6.5-V2-reorganize — Reorganização topológica paralela baseada em densidade. | |
| Implementa o padrão ReorganizingSOM4D: identifica zonas frias (neurônios | |
| mortos) via hit_map e as desloca matematicamente em direção ao centro | |
| de massa ponderado pelos neurônios ativos. | |
| Vantagens sobre revive_dead_neurons puro: | |
| 1. Não zera bmu_win_count (preserva histórico de ativação) | |
| 2. Pulso suave e contínuo (não reinicialização abrupta) | |
| 3. Compatível com paralelismo matricial — uma única operação vetorial | |
| 4. Decaimento temporal: regularization_factor *= decay (early epochs | |
| reorganizam drasticamente, late epochs estabilizam) | |
| 5. Eliminação efetiva de neurônios mortos sem quebrar a topologia | |
| Args: | |
| regularization_factor: magnitude do pull (default 0.05 — 5% do | |
| deslocamento em direção ao centro de massa). | |
| min_dead_rate_for_trigger: só reorganiza se dead_rate > threshold | |
| (default 0.3 = 30% mortos). Evita reorganização desnecessária. | |
| Returns: | |
| Dict com: reorganized, dead_rate_before, dead_rate_after (estimado), | |
| n_dead_pulled, density_center_norm, regularization_factor. | |
| """ | |
| with torch.no_grad(): | |
| # Computa dead rate atual | |
| n_total = self.n_neurons | |
| n_active = int((self.bmu_win_count > 0).sum().item()) | |
| dead_rate = float(1.0 - n_active / max(1, n_total)) | |
| if dead_rate < min_dead_rate_for_trigger: | |
| return { | |
| "reorganized": False, | |
| "reason": f"dead_rate={dead_rate:.3f} < threshold={min_dead_rate_for_trigger}", | |
| "dead_rate": dead_rate, | |
| "n_active": n_active, | |
| "n_total": n_total, | |
| } | |
| # Normaliza hit_map para distribuição de densidade | |
| total_hits = float(self.bmu_win_count.sum().item()) | |
| if total_hits < 1e-8: | |
| # Sem hits — não há centro de massa para puxar | |
| return { | |
| "reorganized": False, | |
| "reason": "no_hits_in_buffer", | |
| "dead_rate": dead_rate, | |
| "n_active": n_active, | |
| "n_total": n_total, | |
| } | |
| density = self.bmu_win_count.float() / total_hits # (I, J, K, L) | |
| # Planifica para operações matriciais | |
| flat_weights = self.weights.view(-1, 4) # (N, 4) | |
| flat_density = density.view(-1, 1) # (N, 1) | |
| # Centro de massa ponderado por densidade (âncora global) | |
| density_center = torch.sum(flat_weights * flat_density, dim=0, keepdim=True) # (1, 4) | |
| density_center_norm = float(torch.norm(density_center).item()) | |
| # Decaimento temporal: reorganização forte em épocas iniciais, | |
| # estabiliza em épocas finais (decay = exp(-t / 5000)) | |
| decay = float(math.exp(-self.t / 5000.0)) | |
| effective_factor = float(regularization_factor * decay) | |
| # Força de atração: neurônios com baixa densidade sofrem maior deslocamento | |
| pull_force = (1.0 - flat_density) * effective_factor # (N, 1) | |
| # Aplica reorganização em uma operação vetorial | |
| new_flat_weights = flat_weights + pull_force * (density_center - flat_weights) | |
| # Restaura shape original | |
| self.weights = new_flat_weights.view(self.I, self.J, self.K, self.L, 4).clone() | |
| self.weights = torch.clamp(self.weights, -100.0, 100.0) | |
| # Sanitiza NaN/Inf | |
| if torch.isnan(self.weights).any() or torch.isinf(self.weights).any(): | |
| self.weights = torch.nan_to_num( | |
| self.weights, nan=0.0, posinf=100.0, neginf=-100.0 | |
| ) | |
| # Conta quantos neurônios mortos foram efetivamente deslocados | |
| n_dead_pulled = int((pull_force.view(-1) > 1e-6).sum().item()) | |
| return { | |
| "reorganized": True, | |
| "dead_rate_before": dead_rate, | |
| "n_dead_pulled": n_dead_pulled, | |
| "density_center_norm": density_center_norm, | |
| "regularization_factor": effective_factor, | |
| "decay": decay, | |
| "n_active": n_active, | |
| "n_total": n_total, | |
| "method": "density_based_reorganization", | |
| } | |
| def auto_adjust_conscience_gamma(self) -> Dict[str, Any]: | |
| """V6.5-V2-auto-conscience-v2 — Ajusta γ dinamicamente conforme ativação. | |
| User requirement: "aprimorar conscience_gamma para ajuste automático | |
| conforme a quantidade de neurônios esteja caindo". | |
| V6.5-V2-auto-conscience-v2 (FIX slow EMA convergence): | |
| A versão anterior usava η=0.05 (EMA suave) que levava 50+ updates | |
| para convergir ao target. Como FASE1 tem apenas ~30-50 updates por | |
| chunk, o γ ficava preso em 0.1 (valor inicial) e nunca atingia o | |
| γ_target=1.0 necessário para reviver neurônios mortos. | |
| Nova estratégia: | |
| - Se r < 0.05 (colapso crítico): salta diretamente para γ_max | |
| (intervenção máxima imediata — sem EMA, bypass) | |
| - Se r < 0.20 (degradação severa): η_eff = 0.5 (convergência rápida) | |
| - Caso contrário: η_eff = η_default = 0.05 (suavização normal) | |
| Math (DeSieno 1988 + adaptação adaptativa): | |
| r = n_active / n_total (∈ [0,1]) | |
| γ_base = γ_max · (1 - r)^2 (intervenção | |
| cresce com | |
| colapso) | |
| drop = max(0, r_prev - r) (tendência de | |
| queda) | |
| γ_boost = 0.3 · drop (reforço | |
| antecipado) | |
| γ_target = clip(γ_base + γ_boost, γ_min, γ_max) | |
| γ_new = (1 - η_eff) · γ_old + η_eff · γ_target (EMA adaptativo) | |
| Comportamento esperado: | |
| r = 1.0 (saudável) → γ_base = 0.00 (data distribution domina) | |
| r = 0.5 (degradação) → γ_base = 0.25 (intervenção moderada) | |
| r = 0.1 (colapso severo) → γ_base = 0.81 (intervenção forte, η=0.5) | |
| r < 0.05 (crítico) → γ = γ_max = 1.0 (salto direto, sem EMA) | |
| Returns: | |
| Dict com: n_active, n_total, activation_rate, prev_activation_rate, | |
| drop, gamma_target, gamma_old, gamma_new, gamma_min, gamma_max, eta. | |
| """ | |
| with torch.no_grad(): | |
| n_total = self.n_neurons | |
| n_active = int((self.bmu_win_count > 0).sum().item()) | |
| r = float(n_active / max(1, n_total)) | |
| # Tendência: se _prev_n_active_rate < 0, ainda não houve ajuste | |
| r_prev = ( | |
| self._prev_n_active_rate | |
| if self._prev_n_active_rate >= 0 | |
| else r | |
| ) | |
| drop = max(0.0, r_prev - r) | |
| # γ alvo: intervenção quadrática no colapso + boost antecipado | |
| gamma_base = self._conscience_gamma_max * (1.0 - r) ** 2 | |
| gamma_trend_boost = 0.3 * drop | |
| gamma_target = max( | |
| self._conscience_gamma_min, | |
| min( | |
| self._conscience_gamma_max, | |
| gamma_base + gamma_trend_boost, | |
| ), | |
| ) | |
| # V6.5-V2-auto-conscience-v2 — adaptive EMA rate | |
| # Critical collapse: jump directly to γ_max (bypass EMA) | |
| # Severe degradation: fast EMA (η=0.5) | |
| # Normal: slow EMA (η=0.05, default) | |
| gamma_old = float(self.conscience_gamma) | |
| if r < 0.05: | |
| # CRITICAL: bypass EMA, jump directly to target | |
| eta_eff = 1.0 | |
| gamma_new = float(gamma_target) | |
| elif r < 0.20: | |
| # SEVERE: fast EMA convergence | |
| eta_eff = 0.5 | |
| gamma_new = (1.0 - eta_eff) * gamma_old + eta_eff * float(gamma_target) | |
| else: | |
| # NORMAL: slow EMA | |
| eta_eff = self._conscience_eta | |
| gamma_new = (1.0 - eta_eff) * gamma_old + eta_eff * float(gamma_target) | |
| gamma_new = float(max( | |
| self._conscience_gamma_min, | |
| min(self._conscience_gamma_max, gamma_new), | |
| )) | |
| # Persiste estado | |
| self.conscience_gamma = gamma_new | |
| self._prev_n_active = n_active | |
| self._prev_n_active_rate = r | |
| self._last_gamma_target = float(gamma_target) | |
| self._last_gamma_drop = float(drop) | |
| self._last_activation_rate = r | |
| return { | |
| "n_active": n_active, | |
| "n_total": n_total, | |
| "activation_rate": r, | |
| "prev_activation_rate": r_prev, | |
| "drop": drop, | |
| "gamma_target": float(gamma_target), | |
| "gamma_old": gamma_old, | |
| "gamma_new": gamma_new, | |
| "gamma_min": float(self._conscience_gamma_min), | |
| "gamma_max": float(self._conscience_gamma_max), | |
| "eta": float(eta_eff), | |
| "bypass_ema": bool(r < 0.05), | |
| } | |
| def update_weights(self, x: torch.Tensor, bmu_idx, accumulate_fisher=False): | |
| """Update Kohonen: ΔW = α·Λ·(x - W) + penalidade EWC em w. | |
| V6.5-V2-metrics-FIX-4 — agora chama update_win_frequency automaticamente | |
| após o update, garantindo que o conscience mechanism seja atualizado | |
| a cada amostra processada (sem necessidade de chamada externa). | |
| V6.5-V2-metrics-FIX-3 — Correções matemáticas: | |
| 1. Floors explícitos em α_t e σ_t (previnem colapso topológico | |
| após muitas épocas, quando σ_t→0 degenera o SOM em k-means). | |
| 2. Sanitização de NaN/Inf em x ANTES do update (uma única amostra | |
| NaN não deve contaminar permanentemente os pesos). | |
| 3. Clamp dos pesos após update (evita explosão numérica). | |
| V6.6 — Fisher accumulation CONTÍNUA (online EWC pattern): | |
| Quando self.fisher_continuous=True (default), acumula Fisher em | |
| TODOS os updates, não apenas quando accumulate_fisher=True. | |
| Após set_ewc_reference(), Fisher é resetado e começa a acumular | |
| novamente para a próxima tarefa. Isto suporta online EWC | |
| (Schwarz et al. 2018) onde múltiplas tarefas são consolidadas | |
| sequencialmente sem catastrophic forgetting. | |
| Args: | |
| x: tensor [4] — amostra 4D. | |
| bmu_idx: (i, j, k, l) — índice do BMU. | |
| accumulate_fisher: se True (ou fisher_continuous=True), acumula | |
| (x_w - W_w)² nos Fisher accumulators. | |
| """ | |
| # V6.5-V2-metrics-FIX-3 — sanitiza x: substitui NaN/Inf por 0 | |
| # (uma amostra degenerada não deve contaminar todo o SOM). | |
| if torch.isnan(x).any() or torch.isinf(x).any(): | |
| x = torch.nan_to_num(x, nan=0.0, posinf=1e4, neginf=-1e4) | |
| dist_sq = self._neighborhood(bmu_idx) | |
| # V6.5-V2-metrics-FIX-3 — floors explícitos (previnem σ_t→0) | |
| sigma = max(self.sigma0 * math.exp(-self.t / 1000), 0.1) | |
| alpha = max(self.alpha0 * math.exp(-self.t / 2000), 0.001) | |
| h = torch.exp(-dist_sq / (2 * sigma ** 2)) | |
| delta = x - self.weights | |
| update = alpha * h.unsqueeze(-1) * delta | |
| if self.old_weights_w is not None and self.fisher_w is not None: | |
| # Penalidade EWC apenas na 4ª dimensão (w) | |
| # ∂L_ewc/∂W_w = λ · F · (W_w - W*_w) → subtraído do update | |
| ewc_penalty = self.lambda_ewc * self.fisher_w * ( | |
| self.weights[..., 3] - self.old_weights_w | |
| ) | |
| update[..., 3] = update[..., 3] - ewc_penalty | |
| self.weights = self.weights + update | |
| # V6.5-V2-metrics-FIX-3 — clamp pós-update para evitar explosão | |
| # (pesos Kohonen são tipicamente O(1); permitir valores > 100 indica | |
| # divergência e propaga NaN para todas as métricas subsequentes). | |
| self.weights = torch.clamp(self.weights, -100.0, 100.0) | |
| # V6.5-V2-metrics-FIX-3 — se ainda assim surgir NaN, reverte este update | |
| if torch.isnan(self.weights).any() or torch.isinf(self.weights).any(): | |
| self.weights = torch.nan_to_num(self.weights, nan=0.0, posinf=100.0, neginf=-100.0) | |
| # V6.6 — Fisher accumulation contínua (online EWC) | |
| # Acumula SEMPRE que: accumulate_fisher=True OU fisher_continuous=True | |
| should_accumulate = accumulate_fisher or getattr(self, 'fisher_continuous', False) | |
| if should_accumulate: | |
| # Acumula Fisher apenas em neurônios próximos ao BMU (Λ > 0.1) | |
| mask = h > 0.1 | |
| if mask.any(): | |
| diff_sq = (x[3] - self.weights[mask][..., 3]) ** 2 | |
| self.fisher_accum[mask] = self.fisher_accum[mask] + diff_sq | |
| self.fisher_count[mask] = self.fisher_count[mask] + 1 | |
| self.t += 1 | |
| # V6.5-V2-metrics-FIX-4 — atualiza conscience mechanism (win frequency) | |
| # automaticamente após cada update. Isto garante que o bias de consciência | |
| # seja aplicado corretamente no próximo find_bmu, forçando distribuição | |
| # uniforme de BMU entre os 864 neurônios. | |
| try: | |
| self.update_win_frequency(bmu_idx) | |
| except Exception: | |
| # Não deixa falha no conscience quebrar o treino principal | |
| pass | |
| # V6.5-V2-auto-conscience — ajuste adaptativo de γ (throttled). | |
| # Roda a cada _auto_adjust_interval updates (default 50) para | |
| # evitar custo computacional em cada amostra e suavizar o ajuste. | |
| try: | |
| if self.t % self._auto_adjust_interval == 0: | |
| self.auto_adjust_conscience_gamma() | |
| except Exception: | |
| # Não deixa falha no auto-ajuste quebrar o treino principal | |
| pass | |
| def finalize_fisher(self): | |
| """Fisher = mean((x_w - W_w)²) sobre samples acumuladas. | |
| V6.6 — se fisher_continuous=True E já existe fisher_w (consolidação | |
| anterior), faz EMA (exponential moving average) com a Fisher anterior | |
| em vez de substituir — isto preserva informação de tarefas passadas | |
| (online EWC pattern, cf. Schwarz et al. 2018). | |
| """ | |
| cnt = self.fisher_count.clamp(min=1e-8) | |
| new_fisher = self.fisher_accum / cnt | |
| if (getattr(self, 'fisher_continuous', False) | |
| and self.fisher_w is not None | |
| and getattr(self, 'fisher_consolidation_count', 0) > 0): | |
| # EMA: F_t = α·F_t_new + (1-α)·F_{t-1} | |
| # α=0.5 dá peso igual a tarefa atual e à memória das anteriores | |
| ema_alpha = 0.5 | |
| self.fisher_w = ema_alpha * new_fisher + (1 - ema_alpha) * self.fisher_w | |
| else: | |
| self.fisher_w = new_fisher | |
| def set_ewc_reference(self): | |
| """Consolida W_w como referência EWC e finaliza Fisher. | |
| V6.6 — Após consolidação, Fisher é RESETADO para acumular informação | |
| da PRÓXIMA tarefa (online EWC pattern). A Fisher consolidada é | |
| preservada em self.fisher_w via EMA em finalize_fisher(). | |
| """ | |
| self.old_weights_w = self.weights[..., 3].clone() | |
| self.finalize_fisher() | |
| # Reset dos accumuladores para a próxima tarefa | |
| self.fisher_accum.zero_() | |
| self.fisher_count.zero_() | |
| # V6.6 — Track consolidation count for EMA logic | |
| if not hasattr(self, 'fisher_consolidation_count'): | |
| self.fisher_consolidation_count = 0 | |
| self.fisher_consolidation_count += 1 | |
| # ------------------------------------------------------------------ | |
| # Métricas para monitoramento (V6.4) | |
| # ------------------------------------------------------------------ | |
| def get_metrics(self) -> dict: | |
| """Retorna métricas atuais do SOM para monitoramento.""" | |
| sigma_t = self.sigma0 * math.exp(-self.t / 1000) | |
| alpha_t = self.alpha0 * math.exp(-self.t / 2000) | |
| # V6.5-V2-metrics-FIX-4 — floors aplicados (consistência com update_weights) | |
| sigma_t_eff = max(sigma_t, 0.1) | |
| alpha_t_eff = max(alpha_t, 0.001) | |
| # V6.5-V2-metrics-FIX-4 — estatísticas do conscience mechanism | |
| n_total = self.n_neurons | |
| n_active = int((self.bmu_win_count > 0).sum().item()) | |
| n_dead = n_total - n_active | |
| win_counts_flat = self.bmu_win_count.flatten().float() | |
| return { | |
| "t": int(self.t), | |
| "sigma_t": float(sigma_t), | |
| "alpha_t": float(alpha_t), | |
| "sigma_t_effective": float(sigma_t_eff), | |
| "alpha_t_effective": float(alpha_t_eff), | |
| "sigma0": float(self.sigma0), | |
| "alpha0": float(self.alpha0), | |
| "lambda_ewc": float(self.lambda_ewc), | |
| "grid_shape": [int(self.I), int(self.J), int(self.K), int(self.L)], | |
| "n_neurons": int(self.I * self.J * self.K * self.L), | |
| "has_ewc_reference": self.old_weights_w is not None, | |
| "fisher_w_mean": ( | |
| float(self.fisher_w.mean().item()) | |
| if self.fisher_w is not None | |
| else 0.0 | |
| ), | |
| "fisher_w_max": ( | |
| float(self.fisher_w.max().item()) | |
| if self.fisher_w is not None | |
| else 0.0 | |
| ), | |
| "fisher_accum_count": int(self.fisher_count.sum().item()), | |
| "weights_norm": float(self.weights.norm().item()), | |
| "weights_w_mean": float(self.weights[..., 3].mean().item()), | |
| # V6.5-V2-metrics-FIX-4 — conscience mechanism status | |
| "conscience_gamma": float(self.conscience_gamma), | |
| "conscience_beta": float(self.conscience_beta), | |
| "n_active_neurons": n_active, | |
| "n_dead_neurons": n_dead, | |
| "neuron_activation_rate": float(n_active / max(1, n_total)), | |
| "bmu_win_count_mean": float(win_counts_flat.mean().item()) if n_total > 0 else 0.0, | |
| "bmu_win_count_max": float(win_counts_flat.max().item()) if n_total > 0 else 0.0, | |
| "win_frequency_mean": float(self.win_frequency.mean().item()), | |
| "win_frequency_max": float(self.win_frequency.max().item()), | |
| "win_frequency_min": float(self.win_frequency.min().item()), | |
| # V6.5-V2-auto-conscience — telemetria do ajuste adaptativo de γ | |
| "conscience_gamma_min": float(self._conscience_gamma_min), | |
| "conscience_gamma_max": float(self._conscience_gamma_max), | |
| "conscience_gamma_target": float(self._last_gamma_target), | |
| "conscience_gamma_drop": float(self._last_gamma_drop), | |
| "conscience_gamma_eta": float(self._conscience_eta), | |
| "conscience_gamma_adjust_interval": int(self._auto_adjust_interval), | |
| "activation_rate_at_last_adjust": float(self._last_activation_rate), | |
| "prev_n_active_at_last_adjust": int(self._prev_n_active), | |
| } | |
| # ============================================================================ | |
| # Classificador de hipótese (8 camadas FC) | |
| # ============================================================================ | |
| class HypothesisClassifier(nn.Module): | |
| """Classificador de hipótese: 8 camadas FC + ReLU + output logit. | |
| Arquitetura: [input → 512 → 256 → 128 → 64 → 32 → 16 → 8] + ReLU | |
| + [8 → 1] (logit) | |
| Loss: BCEWithLogitsLoss | |
| """ | |
| def __init__(self, input_dim, hidden_dims=[512, 256, 128, 64, 32, 16, 8]): | |
| super().__init__() | |
| layers = [] | |
| prev = input_dim | |
| for h in hidden_dims: | |
| layers.append(nn.Linear(prev, h)) | |
| layers.append(nn.ReLU()) | |
| prev = h | |
| layers.append(nn.Linear(prev, 1)) | |
| self.net = nn.Sequential(*layers) | |
| def forward(self, x): | |
| return self.net(x).squeeze(-1) | |
| # ============================================================================ | |
| # Sistema de aprendizado completo (com w temporal e condição de início por N) | |
| # ============================================================================ | |
| class KohonenLearningSystem: | |
| """Pipeline integrado: tokenizer + embedding + SOM4D + classifier + punishment. | |
| V6.5: + VQ-VAE-2 compressor (opcional) + reasoning_engine (opcional) | |
| Args: | |
| vocab_size: tamanho do vocabulário BBPE (default 16384). | |
| hidden_dim: dimensão do embedding (default 1024). | |
| seq_len: comprimento máximo da sequência (default 8). | |
| som_grid: (I, J, K, L) — grid 4D do SOM (default CANÔNICO (4, 4, 4, 4) = 256). | |
| alpha0, sigma0: hiperparâmetros do SOM. | |
| lambda_ewc: peso da penalidade EWC. | |
| N_start: threshold do histograma para iniciar treino. | |
| dim_choice: 'x' | 'y' | 'z' — dimensão usada no histograma. | |
| hypothesis_hidden: arquitetura do HypothesisClassifier. | |
| T_max: normalização temporal (w = time_step / T_max). | |
| enable_vqvae2 (V6.5): ativa VQ-VAE-2 compressor no pipeline. | |
| enable_reasoning (V6.5): ativa reasoning_engine integrado. | |
| vqvae2_code_dim (V6.5): dimensão do codebook do VQ-VAE-2. | |
| vqvae2_num_codes (V6.5): tamanho do codebook top+bottom. | |
| """ | |
| def __init__( | |
| self, | |
| vocab_size=16384, | |
| hidden_dim=1024, | |
| seq_len=8, | |
| som_grid=(4, 4, 4, 4), | |
| alpha0=0.5, | |
| sigma0=2.0, | |
| lambda_ewc=0.02, | |
| N_start=10, | |
| dim_choice="y", | |
| hypothesis_hidden=[512, 256, 128, 64, 32, 16, 8], | |
| T_max=10000, | |
| # V6.5 — VQ-VAE-2 + reasoning_engine | |
| enable_vqvae2: bool = True, | |
| # V6.5-V2 — reasoning_engine desativado por padrão (user requirement: | |
| # "por enquanto manter reasoning_engine desativado"). O ReasoningEngine | |
| # cria ThreadPoolExecutor(4 workers) + ToolAgentCoordinator que consome | |
| # ~100-200MB adicionais e não afeta o aprendizado do SOM. Para reativar, | |
| # passe enable_reasoning=True explicitamente (requer ≥6GB cgroup). | |
| enable_reasoning: bool = False, | |
| vqvae2_code_dim: int = 16, | |
| vqvae2_num_codes_top: int = 64, | |
| vqvae2_num_codes_bot: int = 128, | |
| # V6.5-final — SmoothQuant W8A8 compressor for SOM weights | |
| enable_w8a8: bool = True, | |
| w8a8_alpha: float = 0.5, | |
| w8a8_n_bits: int = 8, | |
| w8a8_calibration_samples: int = 128, | |
| # V6.5-attn — MultiHeadAttention integrado ao text_to_4d_vector | |
| # User requirement: "verificar se o mecanismo de atenção está ativo | |
| # e acessado logicamente funcional" | |
| enable_attention: bool = True, | |
| attention_n_heads: int = 8, | |
| ): | |
| self.tokenizer = SimpleBBPETokenizer(vocab_size) | |
| self.embedding = nn.Embedding(vocab_size, hidden_dim) | |
| self.hidden_dim = hidden_dim | |
| self.seq_len = seq_len | |
| self.T_max = T_max | |
| self.time_counter = 0 # contador global de amostras processadas | |
| self.som = KohonenSOM4D(som_grid, alpha0, sigma0, lambda_ewc) | |
| self.som_grid = som_grid | |
| self.som_neuron_count = ( | |
| som_grid[0] * som_grid[1] * som_grid[2] * som_grid[3] | |
| ) | |
| self.classifier: Optional[HypothesisClassifier] = None | |
| self.hypothesis_hidden = hypothesis_hidden | |
| self.classifier_trained = False | |
| self.buffer_4d = [] | |
| self.buffer_labels = [] | |
| # V6.5-V2-memfix — Sliding window limit for buffer_4d to prevent OOM. | |
| # User requirement: "resolver falhas de lógica e de bugs que estejam | |
| # causando alto consumo de memória sem distorcer a arquitetura Kohonen". | |
| # The buffer stores vec_4d tensors for SOM training and neuron labeling. | |
| # Without a limit, streaming 8000+ samples accumulates 8000 tensors, | |
| # each retaining the embedding computation graph (16M params), causing | |
| # OOM (3.5GB RSS observed). Sliding window keeps recent samples | |
| # for SOM updates while bounding memory. The Kohonen architecture (SOM | |
| # grid, BMU, Gaussian neighborhood, EWC) is NOT changed. | |
| # V6.5-V4-canonical-256: buffer_max_size = 256 (CANÔNICO, alinhado ao grid). | |
| # User requirement EXATO (V6.5-V4): "fazer (tornar canônico) buffer 256 e | |
| # grid para (4,4,4,4)=256". Buffer e grid agora têm o MESMO tamanho (256), | |
| # eliminando o desbalanceamento que causava OOM em V6.5-V3 (buffer=864 com | |
| # grid=256). A correspondência 1:1 entre amostras no buffer e neurônios no | |
| # grid 4D é matematicamente elegante — cada amostra pode, em média, ativar | |
| # um neurônio distinto, maximizando a utilização do mapa Kohonen. | |
| # OOM-safety garantida por: | |
| # (a) VQ-VAE-2 lazy compression (a cada 16 add_data) — ver _vqvae2_call_count | |
| # (b) torch.no_grad() em toda compressão | |
| # (c) gc.collect() a cada 4 batches (no train script) | |
| # (d) OomGuard thread daemon (max_rss_mb=2500) | |
| # (e) check_memory_and_maybe_fallback() reduz para 128 se RSS > 75% | |
| # Grid SOM (4,4,4,4)=256 é CANÔNICO e PERMANECE em ambos os modos. | |
| self.buffer_max_size = 256 | |
| self._buffer_max_size_canonical = 256 | |
| self._buffer_max_size_fallback = 128 | |
| # V6.5-V3-no-regression: VQ-VAE-2 lazy compression counter. | |
| # Comprimir a cada add_data causava OOM (200MB+ tensores intermediários | |
| # por chamada). Agora comprime a cada 16 add_data — mesma cobertura | |
| # estatística, 16x menos pressão de memória. | |
| self._vqvae2_call_count = 0 | |
| self._vqvae2_lazy_interval = 16 | |
| self.training_ready = False | |
| self.N = N_start | |
| self.dim_choice = dim_choice | |
| self.dim_index = {"x": 0, "y": 1, "z": 2}[dim_choice] | |
| self.punishment_count = 0 | |
| self.success_count = 0 | |
| self.histogram = Counter() | |
| self.required_new_samples = 0 | |
| # ------------------------------------------------------------------ | |
| # V6.5-fix — Dynamic Label Registry (replaces hardcoded 'gato'/'cachorro') | |
| # ------------------------------------------------------------------ | |
| # User requirement: "os strings 'gato' e 'cachorro' são fixos quando | |
| # deveriam ser extrações variáveis e flexíveis de rótulos proveniente | |
| # de dados dos datasets anteriormente treinados" | |
| # | |
| # Mathematical analysis: | |
| # During training, add_data() receives integer labels (0/1) produced | |
| # by make_label(text) — these integers are dataset-agnostic. The | |
| # semantic meaning of "0" vs "1" depends on the dataset: | |
| # - punctuation datasets: 0 = "unpunctuated", 1 = "punctuated" | |
| # - chat datasets: 0 = "user_turn", 1 = "assistant_turn" | |
| # - instruct datasets: 0 = "instruction", 1 = "response" | |
| # - news datasets: 0 = "headline", 1 = "body" | |
| # - corpus datasets: 0 = "raw_text", 1 = "compressed_text" | |
| # | |
| # The label_registry stores, per dataset, the {label_int: label_str} | |
| # mapping. predict() looks up the dynamic string instead of returning | |
| # a hardcoded "gato"/"cachorro". | |
| # | |
| # Math: label_str = registry[dataset_name][label_int] | |
| # where label_int = 1 if sigmoid(logit) > 0.5 else 0 | |
| # | |
| # Fallback chain (when no dataset-specific registry exists): | |
| # 1. Most recently registered dataset mapping. | |
| # 2. Default {0: "negative", 1: "positive"}. | |
| # 3. Synthetic "label_{int}" if all else fails. | |
| self.label_registry: Dict[str, Dict[int, str]] = {} | |
| self.default_label_strings: Dict[int, str] = { | |
| 0: "negative", | |
| 1: "positive", | |
| } | |
| self.last_dataset_used: Optional[str] = None | |
| # V6.5-fix — Inference punishment tracking (mirrors training protocol) | |
| # User requirement: "ao punir o modelo reajusta seus parâmetros usando | |
| # a camada de hipótese para funcionar como na lógica de punição e | |
| # ajuste de parâmetros durante o treinamento" | |
| self.inference_punishment_count: int = 0 | |
| self.inference_punishment_history: List[Dict[str, Any]] = [] | |
| # ------------------------------------------------------------------ | |
| # V6.5 — VQ-VAE-2 compressor (ativa efetiva no pipeline) | |
| # ------------------------------------------------------------------ | |
| self.enable_vqvae2 = enable_vqvae2 | |
| self.vqvae2_compressor = None | |
| self.vqvae2_metrics_history: List[Dict[str, Any]] = [] | |
| if enable_vqvae2: | |
| try: | |
| from .vqvae2_hierarchical_flexnet import HierarchicalVQVAE2 | |
| # Modalidade única: "som_4d" com input_dim=4 | |
| self.vqvae2_compressor = HierarchicalVQVAE2( | |
| modalities={"som_4d": 4}, | |
| code_dim=vqvae2_code_dim, | |
| num_codes_top=vqvae2_num_codes_top, | |
| num_codes_bot=vqvae2_num_codes_bot, | |
| hidden=32, | |
| beta=0.25, | |
| ema_decay=0.99, | |
| dead_code_threshold=1.0, | |
| dead_code_restart_every=3, | |
| goose_temp_init=2.0, | |
| goose_temp_final=0.5, | |
| goose_schedule="cosine", | |
| total_epochs=25, | |
| norm_type="none", | |
| rmsnorm_in_vq=False, | |
| ) | |
| # Inicia em modo treino para ativar EMA updates | |
| self.vqvae2_compressor.train() | |
| except Exception as e: | |
| # Fallback: desabilita VQ-VAE-2 se houver erro de import | |
| self.enable_vqvae2 = False | |
| self.vqvae2_compressor = None | |
| import warnings | |
| warnings.warn(f"VQ-VAE-2 disabled: {e}") | |
| # ------------------------------------------------------------------ | |
| # V6.5-final — SmoothQuantCompressor for REAL W8A8 of SOM weights | |
| # ------------------------------------------------------------------ | |
| # User requirement: "integrar SmoothQuantCompressor ao KLS para | |
| # compressão W8A8 real dos pesos do SOM" | |
| # | |
| # O SmoothQuantCompressor é instanciado e calibrado sobre os pesos | |
| # atuais do SOM (self.som.weights reshape para [N_neurons, 4]) usando | |
| # o buffer_4d como amostras de ativação reais. Após calibração, o | |
| # método compress_som_weights_w8a8() aplica quantização INT8 simétrica | |
| # (com smoothing) e dequantização imediata, preservando a topologia | |
| # do SOM com erro relativo < 1%. | |
| self.enable_w8a8 = enable_w8a8 | |
| self.w8a8_compressor = None | |
| self.w8a8_metrics_history: List[Dict[str, Any]] = [] | |
| if enable_w8a8: | |
| try: | |
| from ..quantization.smoothquant_compressor import SmoothQuantCompressor | |
| self.w8a8_compressor = SmoothQuantCompressor( | |
| alpha=w8a8_alpha, | |
| n_bits=w8a8_n_bits, | |
| calibration_samples=w8a8_calibration_samples, | |
| ) | |
| except Exception as e: | |
| self.enable_w8a8 = False | |
| self.w8a8_compressor = None | |
| import warnings | |
| warnings.warn(f"SmoothQuantCompressor disabled: {e}") | |
| # ------------------------------------------------------------------ | |
| # V6.5 — ReasoningEngine (integração ativa + tool_coordinator workers reativados) | |
| # ------------------------------------------------------------------ | |
| self.enable_reasoning = enable_reasoning | |
| self.reasoning_engine = None | |
| if enable_reasoning: | |
| try: | |
| from ..reasoning.reasoning_engine import ReasoningEngine | |
| # V6.5-final: REATIVAR tool_coordinator workers — explicit n_workers | |
| # User requirement: "reativar reasoning_engine's tool_coordinator workers" | |
| # O ReasoningEngine cria internamente ToolAgentCoordinator(n_workers=4) | |
| # que mantém um ThreadPoolExecutor ativo. Para garantir que os | |
| # workers sejam de fato utilizados (não apenas instanciados), | |
| # o método _compress_buffer_with_vqvae2 abaixo chama | |
| # execute_parallel() periodicamente durante o treino. | |
| self.reasoning_engine = ReasoningEngine( | |
| max_thinking_steps=10, | |
| max_iterations=3, | |
| convergence_threshold=0.9, | |
| verbose=False, | |
| ) | |
| # Verifica que tool_coordinator + workers estão ativos | |
| tc = self.reasoning_engine.tool_coordinator | |
| self._tool_coordinator_workers_active = ( | |
| tc is not None and tc._executor is not None | |
| ) | |
| # Registra uma ferramenta interna: consultar SOM | |
| def som_query_tool(query: str) -> str: | |
| """Ferramenta: consulta o SOM do KLS para responder.""" | |
| # V6.5-V2-metrics-FIX-2 — no_grad para evitar building de grafo | |
| # (cada chamada de text_to_4d_vector sem no_grad retém ~100MB | |
| # de grafo computacional: embedding 16M params + attention + SVD) | |
| with torch.no_grad(): | |
| pred = self.predict(query) | |
| bmu_info = "" | |
| if self.buffer_4d: | |
| try: | |
| self.time_counter += 1 | |
| vec = text_to_4d_vector( | |
| query, self.tokenizer, self.embedding, | |
| self.hidden_dim, self.seq_len, | |
| self.time_counter, self.T_max, | |
| ) | |
| bmu = self.som.find_bmu(vec) | |
| bmu_info = f" | BMU={bmu}" | |
| except Exception: | |
| pass | |
| return f"prediction={pred}{bmu_info}" | |
| self.reasoning_engine.register_tool( | |
| "som_query", som_query_tool, | |
| description="Consulta o SOM do KohonenLearningSystem", | |
| timeout_s=10.0, | |
| ) | |
| # V6.5-final: registra segunda ferramenta para forçar uso | |
| # paralelo dos workers via execute_parallel() | |
| def buffer_stats_tool(query: str) -> str: | |
| """Ferramenta: retorna estatísticas do buffer atual.""" | |
| return ( | |
| f"buffer_size={len(self.buffer_4d)}, " | |
| f"punishment_count={self.punishment_count}, " | |
| f"success_count={self.success_count}, " | |
| f"training_ready={self.training_ready}" | |
| ) | |
| self.reasoning_engine.register_tool( | |
| "buffer_stats", buffer_stats_tool, | |
| description="Estatísticas do buffer interno do KLS", | |
| timeout_s=5.0, | |
| ) | |
| except Exception as e: | |
| self.enable_reasoning = False | |
| self.reasoning_engine = None | |
| self._tool_coordinator_workers_active = False | |
| import warnings | |
| warnings.warn(f"ReasoningEngine disabled: {e}") | |
| # ------------------------------------------------------------------ | |
| # V6.5-attn — MultiHeadAttention integrado ao text_to_4d_vector | |
| # ------------------------------------------------------------------ | |
| # User requirement: "verificar se o mecanismo de atenção está ativo | |
| # e acessado logicamente funcional" | |
| # | |
| # O MultiHeadAttention (de attention_multimodal.py) é instanciado com | |
| # d_model=hidden_dim e n_heads=attention_n_heads. Ele é aplicado em | |
| # text_to_4d_vector entre a fusão (+PE) e o SVD, atuando como | |
| # self-attention residual sobre a sequência embedded. | |
| # | |
| # Métricas (n_calls, last_norm_in/out, last_attn_activated) são | |
| # acumuladas em self.attention_metrics e expostas via | |
| # get_attention_metrics() para verificação de "ativo e funcional". | |
| self.enable_attention = enable_attention | |
| self.attention = None | |
| self.attention_metrics: Dict[str, Any] = { | |
| "n_calls": 0, | |
| "n_errors": 0, | |
| "last_norm_in": 0.0, | |
| "last_norm_out": 0.0, | |
| "last_attn_activated": False, | |
| "last_attn_diff_norm": 0.0, | |
| } | |
| if enable_attention: | |
| try: | |
| from .attention_multimodal import MultiHeadAttention | |
| # d_model = hidden_dim, n_heads = attention_n_heads | |
| # assert hidden_dim % n_heads == 0 | |
| n_heads = attention_n_heads | |
| if hidden_dim % n_heads != 0: | |
| # ajusta para divisor válido mais próximo | |
| for cand in [8, 4, 2, 1]: | |
| if hidden_dim % cand == 0: | |
| n_heads = cand | |
| break | |
| self.attention = MultiHeadAttention( | |
| d_model=hidden_dim, | |
| n_heads=n_heads, | |
| dropout=0.0, | |
| max_chunk=4096, | |
| ) | |
| # Em eval mode para determinismo (não treinamos o attention separadamente) | |
| self.attention.eval() | |
| self.attention_n_heads = n_heads | |
| except Exception as e: | |
| self.enable_attention = False | |
| self.attention = None | |
| import warnings | |
| warnings.warn(f"MultiHeadAttention disabled: {e}") | |
| def get_attention_metrics(self) -> Dict[str, Any]: | |
| """V6.5-attn — Retorna métricas do MultiHeadAttention integrado. | |
| User requirement: "verificar se o mecanismo de atenção está ativo | |
| e acessado logicamente funcional" | |
| Returns: | |
| Dict com: | |
| - active: bool — attention module está instanciado | |
| - n_calls: int — número de chamadas forward | |
| - n_errors: int — número de erros silenciados | |
| - last_norm_in / last_norm_out: normas do input/output | |
| - last_attn_activated: bool — output é finito e diferente do input | |
| - last_attn_diff_norm: float — ||attn_out - input|| | |
| - logic_functional: bool — n_calls > 0 AND n_errors == 0 | |
| AND last_attn_activated == True | |
| """ | |
| m = self.attention_metrics | |
| return { | |
| "active": bool(self.attention is not None), | |
| "n_calls": int(m.get("n_calls", 0)), | |
| "n_errors": int(m.get("n_errors", 0)), | |
| "last_norm_in": float(m.get("last_norm_in", 0.0)), | |
| "last_norm_out": float(m.get("last_norm_out", 0.0)), | |
| "last_attn_activated": bool(m.get("last_attn_activated", False)), | |
| "last_attn_diff_norm": float(m.get("last_attn_diff_norm", 0.0)), | |
| "n_heads": int(getattr(self, "attention_n_heads", 0)), | |
| # "logicamente funcional" = foi chamado, sem erros, e última chamada | |
| # produziu output finito e diferente do input | |
| "logic_functional": bool( | |
| self.attention is not None | |
| and m.get("n_calls", 0) > 0 | |
| and m.get("n_errors", 0) == 0 | |
| and m.get("last_attn_activated", False) | |
| ), | |
| } | |
| # ================================================================== | |
| # V6.5-fix — Dynamic Label Registry API | |
| # ================================================================== | |
| # User requirement: "os strings 'gato' e 'cachorro' são fixos quando | |
| # deveriam ser extrações variáveis e flexíveis de rótulos proveniente | |
| # de dados dos datasets anteriormente treinados" | |
| # | |
| # Mathematical formalization: | |
| # Let D = {d_1, d_2, ..., d_K} be the set of datasets seen during | |
| # training. For each d_k, the training script provides a mapping | |
| # μ_k : {0, 1} → Σ (where Σ is the string-label alphabet). | |
| # | |
| # The label_registry stores R = {(d_k, μ_k) : k = 1..K}. | |
| # | |
| # At inference time, given a query sentence s: | |
| # 1. vec_4d = text_to_4d_vector(s, ...) | |
| # 2. p = _predict_single(vec_4d) ∈ [0, 1] | |
| # 3. label_int = 1 if p > 0.5 else 0 | |
| # 4. label_str = μ_last(label_int) [fallback chain if unknown] | |
| # | |
| # If dataset_name is provided at inference, uses μ_dataset_name; | |
| # otherwise uses the most recently registered μ (assumes temporal | |
| # coherence: latest trained dataset is most relevant). | |
| # ------------------------------------------------------------------ | |
| def register_dataset_labels( | |
| self, | |
| dataset_name: str, | |
| label_strings: Dict[int, str], | |
| ) -> None: | |
| """V6.5-fix — Registra mapeamento {label_int: label_str} para um dataset. | |
| Args: | |
| dataset_name: nome do dataset (ex: 'dominguesm/restore-punctuation-ptbr-dataset'). | |
| label_strings: dict {0: 'unpunctuated', 1: 'punctuated'}. | |
| """ | |
| if not isinstance(label_strings, dict): | |
| raise TypeError(f"label_strings must be dict, got {type(label_strings)}") | |
| # Sanitize keys to int, values to str | |
| sanitized = {int(k): str(v) for k, v in label_strings.items()} | |
| self.label_registry[dataset_name] = sanitized | |
| self.last_dataset_used = dataset_name | |
| def get_label_string( | |
| self, | |
| label_int: int, | |
| dataset_name: Optional[str] = None, | |
| ) -> str: | |
| """V6.5-fix — Resolve label_int → label_str dinamicamente. | |
| Fallback chain (mathematically well-defined): | |
| 1. If dataset_name is registered, use registry[dataset_name][label_int]. | |
| 2. Else if any dataset is registered, use the most recent one. | |
| 3. Else use default_label_strings. | |
| 4. Final fallback: synthetic f"label_{label_int}". | |
| Args: | |
| label_int: 0 ou 1. | |
| dataset_name: opcional — nome do dataset para disambiguar. | |
| Returns: | |
| String label correspondente. | |
| """ | |
| label_int = int(label_int) | |
| # 1. Dataset-specific lookup | |
| if dataset_name is not None and dataset_name in self.label_registry: | |
| mapping = self.label_registry[dataset_name] | |
| if label_int in mapping: | |
| return mapping[label_int] | |
| # 2. Most recently registered mapping | |
| if self.label_registry: | |
| last_ds = ( | |
| dataset_name if dataset_name in self.label_registry | |
| else (self.last_dataset_used or list(self.label_registry.keys())[-1]) | |
| ) | |
| if last_ds and last_ds in self.label_registry: | |
| mapping = self.label_registry[last_ds] | |
| if label_int in mapping: | |
| return mapping[label_int] | |
| # 3. Default mapping | |
| if label_int in self.default_label_strings: | |
| return self.default_label_strings[label_int] | |
| # 4. Synthetic fallback | |
| return f"label_{label_int}" | |
| def get_label_registry_state(self) -> Dict[str, Any]: | |
| """V6.5-fix — Retorna estado atual do label_registry para inspeção.""" | |
| return { | |
| "n_datasets_registered": int(len(self.label_registry)), | |
| "datasets": { | |
| ds: {int(k): str(v) for k, v in mapping.items()} | |
| for ds, mapping in self.label_registry.items() | |
| }, | |
| "last_dataset_used": str(self.last_dataset_used) if self.last_dataset_used else None, | |
| "default_label_strings": {int(k): str(v) for k, v in self.default_label_strings.items()}, | |
| "inference_punishment_count": int(self.inference_punishment_count), | |
| "inference_punishment_history_len": int(len(self.inference_punishment_history)), | |
| } | |
| def add_data( | |
| self, | |
| sentences: List[str], | |
| labels: List[int], | |
| label_strings: Optional[Dict[int, str]] = None, | |
| dataset_name: Optional[str] = None, | |
| ): | |
| """V6.5-fix — Adiciona amostras: text → 4D vector + atualiza histograma. | |
| Args: | |
| sentences: lista de textos. | |
| labels: lista de inteiros (0/1). | |
| label_strings: opcional — {0: 'str0', 1: 'str1'} para o dataset. | |
| dataset_name: opcional — nome do dataset (para registrar label_strings). | |
| Mathematical extension (V6.5-fix): | |
| Se label_strings e dataset_name são fornecidos, registra o mapeamento | |
| no label_registry. Isto permite que predict() retorne o rótulo | |
| textual correto ao invés de hardcoded 'gato'/'cachorro'. | |
| """ | |
| # Register label mapping if provided | |
| if label_strings is not None and dataset_name is not None: | |
| self.register_dataset_labels(dataset_name, label_strings) | |
| if dataset_name is not None: | |
| self.last_dataset_used = dataset_name | |
| # V6.5-V2-memfix — Use torch.no_grad() to avoid building computation | |
| # graphs during vec_4d computation. The embedding graph (16M params) | |
| # would otherwise be retained by every tensor in buffer_4d, causing OOM | |
| # with 8000+ samples. The vec_4d is a 4-float summary (x,y,z,w) that | |
| # does NOT need gradient — it feeds into SOM (Hebbian, no backprop) and | |
| # the HypothesisClassifier (which trains its own weights). | |
| with torch.no_grad(): | |
| for sent, lab in zip(sentences, labels): | |
| self.time_counter += 1 | |
| vec = text_to_4d_vector( | |
| sent, | |
| self.tokenizer, | |
| self.embedding, | |
| self.hidden_dim, | |
| self.seq_len, | |
| self.time_counter, | |
| self.T_max, | |
| attention=self.attention, # V6.5-attn | |
| attention_metrics=self.attention_metrics, # V6.5-attn | |
| ) | |
| # CRITICAL: detach to break any residual graph + clone for safety | |
| self.buffer_4d.append(vec.detach().clone()) | |
| self.buffer_labels.append(lab) | |
| dim_val = round(vec[self.dim_index].item(), 2) | |
| self.histogram[dim_val] += 1 | |
| # V6.5-V2-memfix — Enforce sliding window limit | |
| if len(self.buffer_4d) > self.buffer_max_size: | |
| overflow = len(self.buffer_4d) - self.buffer_max_size | |
| self.buffer_4d = self.buffer_4d[-self.buffer_max_size:] | |
| self.buffer_labels = self.buffer_labels[-self.buffer_max_size:] | |
| def check_training_start(self) -> bool: | |
| """Inicia treino quando algum bucket do histograma atinge N.""" | |
| if ( | |
| not self.training_ready | |
| and max(self.histogram.values(), default=0) >= self.N | |
| ): | |
| self.training_ready = True | |
| return True | |
| return False | |
| def train_som_on_buffer(self): | |
| """Treina SOM por 5 épocas sobre o buffer atual (com Fisher accum). | |
| V6.5-V2-memfix — Wrapped in torch.no_grad() to prevent graph building | |
| during SOM Hebbian updates. The SOM uses competitive learning (BMU + | |
| Gaussian neighborhood), not backprop — no gradient is needed. | |
| V6.5-V2-parallel — Agora usa train_batch_parallel (ParallelSOM4D pattern) | |
| ao invés do loop find_bmu + update_weights manual. Isto elimina o | |
| overhead de Python e usa broadcasting PyTorch sobre os 864 neurônios. | |
| User requirement: "Para um ajuste paralelo verdadeiro e massivo de uma | |
| SOM de 4 dimensões, a abordagem ideal é o paralelismo vetorial em GPU | |
| utilizando PyTorch." | |
| V6.5-V2-reorganize — Após treinar, dispara reorganização por densidade | |
| se dead_rate > 30% (ReorganizingSOM4D pattern). | |
| """ | |
| if not self.buffer_4d: | |
| return | |
| # V6.5-V2-memfix — detach+stack to ensure no graph leaks from buffer | |
| data = torch.stack(self.buffer_4d).detach() | |
| with torch.no_grad(): | |
| # V6.5-V2-parallel — Usa train_batch_parallel para todas as 5 épocas | |
| # User requirement: paralelismo vetorial PyTorch sobre os 864 neurônios | |
| acc_fisher = ( | |
| self.punishment_count == 0 | |
| and self.som.old_weights_w is None | |
| ) | |
| for _ in range(5): # épocas de treino rápido | |
| perm = torch.randperm(len(data)) | |
| # train_batch_parallel já faz sanitização NaN/Inf + conscience | |
| # + Fisher accumulation em uma única chamada vetorial | |
| self.som.train_batch_parallel( | |
| samples=data[perm], | |
| use_conscience=True, | |
| accumulate_fisher=acc_fisher, | |
| ) | |
| # V6.5-V2-reorganize — Reorganização topológica por densidade | |
| # User requirement (ReorganizingSOM4D pattern): "Os neurônios pouco | |
| # ativados (isolados) sofrem uma força de atração em direção aos | |
| # agrupamentos de alta densidade, reorganizando geometricamente o | |
| # hipercubo 4D sem perder o alinhamento matricial." | |
| try: | |
| reorg = self.reorganize_map_if_needed( | |
| regularization_factor=0.05, | |
| min_dead_rate_for_trigger=0.3, | |
| min_steps_between_reorganizations=300, | |
| ) | |
| if reorg.get("action") == "auto_reorganized": | |
| import logging as _logging | |
| _log = _logging.getLogger("kohonen_learning_system") | |
| _log.info( | |
| f"[V6.5-V2-reorganize] Map reorganized: " | |
| f"n_dead_pulled={reorg.get('n_dead_pulled')}, " | |
| f"dead_rate={reorg.get('dead_rate_before', 0):.3f}, " | |
| f"factor={reorg.get('regularization_factor', 0):.4f}" | |
| ) | |
| except Exception: | |
| pass | |
| # V6.5-V3-no-regression: VQ-VAE-2 REATIVADO. Compressão agora é LAZY | |
| # (a cada 16 add_data) para evitar OOM. User: "não autorizei a desativação | |
| # do 'VQ-VAE-2'". OOM-safety por lazy compression + no_grad + fallback. | |
| if self.enable_vqvae2 and self.vqvae2_compressor is not None: | |
| self._vqvae2_call_count = getattr(self, "_vqvae2_call_count", 0) + 1 | |
| _lazy_interval = getattr(self, "_vqvae2_lazy_interval", 16) | |
| if self._vqvae2_call_count % _lazy_interval == 0: | |
| with torch.no_grad(): | |
| # Comprime apenas um SLICE do buffer (últimas 64 amostras) | |
| # para limitar tensores intermediários a ~1MB. | |
| _slice = data[-64:] if data.shape[0] > 64 else data | |
| self._compress_buffer_with_vqvae2(_slice) | |
| # V6.5-final — Ativa SmoothQuant W8A8 compressor dos pesos do SOM | |
| # User requirement: "integrar SmoothQuantCompressor ao KLS para | |
| # compressão W8A8 real dos pesos do SOM" | |
| if self.enable_w8a8 and self.w8a8_compressor is not None: | |
| with torch.no_grad(): | |
| self.compress_som_weights_w8a8() | |
| # V6.5-final — Reativa tool_coordinator workers via execute_parallel | |
| # User requirement: "reativar reasoning_engine's tool_coordinator workers" | |
| # O ThreadPoolExecutor (n_workers=4) só é efetivamente utilizado quando | |
| # execute_parallel() é chamado com múltiplas ferramentas. Caso contrário, | |
| # os workers ficam idle e o recurso é desperdiçado. | |
| # | |
| # V6.5-V2-metrics-FIX-2: Reduzido de TODAS as chamadas para a cada 5 | |
| # chamadas (counter-based). O exercício dos workers não é crítico para | |
| # o aprendizado do SOM — é apenas para manter os threads ativos. | |
| # Chamar a cada batch causava OOM (4 parallel predict() calls com | |
| # graph building = 400MB+ por batch). Com no_grad (aplicado acima) + | |
| # redução de frequência, o overhead é mínimo. | |
| self._tool_coordinator_call_count = getattr(self, "_tool_coordinator_call_count", 0) + 1 | |
| if self._tool_coordinator_call_count % 5 == 0: | |
| self._exercise_tool_coordinator_workers() | |
| def _exercise_tool_coordinator_workers(self) -> Dict[str, Any]: | |
| """V6.5-final — Exercita os workers do tool_coordinator em paralelo. | |
| Chama execute_parallel() com todas as ferramentas registradas para | |
| garantir que o ThreadPoolExecutor (n_workers=4) seja efetivamente | |
| utilizado, cumprindo o requisito "reativar tool_coordinator workers". | |
| Returns: | |
| Dict com resultados de cada ferramenta + tempo total. | |
| """ | |
| if ( | |
| not self.enable_reasoning | |
| or self.reasoning_engine is None | |
| ): | |
| return {"active": False, "reason": "reasoning_disabled"} | |
| tc = self.reasoning_engine.tool_coordinator | |
| if tc is None: | |
| return {"active": False, "reason": "no_tool_coordinator"} | |
| try: | |
| import time as _time | |
| t0 = _time.time() | |
| # Submete TODAS as ferramentas registradas em paralelo — isso | |
| # efetivamente acorda os workers do ThreadPoolExecutor. | |
| tool_names = list(tc._tools.keys()) | |
| if not tool_names: | |
| return {"active": True, "n_workers": tc.n_workers, "n_tools": 0} | |
| # Executa em paralelo: cada ferramenta recebe um query dummy | |
| requests = [(name, "status_check") for name in tool_names] | |
| results = tc.execute_parallel(requests, fail_fast=False) | |
| elapsed_ms = (_time.time() - t0) * 1000 | |
| return { | |
| "active": True, | |
| "n_workers": tc.n_workers, | |
| "n_tools": len(tool_names), | |
| "tool_names": tool_names, | |
| "n_results": len(results), | |
| "elapsed_ms": float(elapsed_ms), | |
| "workers_exercised": True, | |
| } | |
| except Exception as e: | |
| return { | |
| "active": False, | |
| "error": str(e)[:200], | |
| "workers_exercised": False, | |
| } | |
| def _compress_buffer_with_vqvae2(self, data: torch.Tensor) -> Dict[str, Any]: | |
| """V6.5 — Comprime buffer 4D via VQ-VAE-2 hierárquico. | |
| Ativa efetivamente o VQ-VAE-2 no pipeline de compressão: | |
| 1. Encoder: (B, 4) → z_e (B, code_dim) | |
| 2. VQ hierárquico: z_e → z_q_top + z_q_bot (codebooks EMA + Goose) | |
| 3. Decoder: z_q_combined → z_recon (B, 4) | |
| 4. Loss: commitment (top+bot) + reconstruction (MSE) | |
| 5. Códigos top/bottom retornados para inspeção | |
| Args: | |
| data: tensor (B, 4) com vetores 4D do buffer. | |
| Returns: | |
| Dict com vqvae2_metrics (também armazenado em vqvae2_metrics_history). | |
| """ | |
| try: | |
| # Sanitiza NaN/Inf | |
| data_clean = torch.nan_to_num(data, nan=0.0, posinf=1e4, neginf=-1e4) | |
| # VQ-VAE-2 espera dict {modality_name: tensor} | |
| batch = {"som_4d": data_clean} | |
| out = self.vqvae2_compressor(batch) | |
| # Incrementa época do VQ (controla schedule Goose + dead code restart) | |
| try: | |
| self.vqvae2_compressor.vq.increment_epoch() | |
| except Exception: | |
| pass | |
| stats = out.get("stats", {}) | |
| vqvae2_metrics = { | |
| "vq_loss": float(out.get("vq_loss", 0.0)), | |
| "recon_loss": float(out.get("recon_loss", 0.0)), | |
| "total_loss": float(out.get("total_loss", 0.0)), | |
| "n_used_top": int(stats.get("n_used_top", 0)), | |
| "n_used_bot": int(stats.get("n_used_bot", 0)), | |
| "usage_ratio_top": float(stats.get("usage_ratio_top", 0.0)), | |
| "usage_ratio_bot": float(stats.get("usage_ratio_bot", 0.0)), | |
| "codebook_ppl_top": float(stats.get("codebook_ppl_top", 0.0)), | |
| "codebook_ppl_bot": float(stats.get("codebook_ppl_bot", 0.0)), | |
| "n_restarted_top": int(stats.get("n_restarted_top", 0)), | |
| "n_restarted_bot": int(stats.get("n_restarted_bot", 0)), | |
| "goose_temp": float(stats.get("goose_temp", 0.0)), | |
| "active": True, | |
| } | |
| self.vqvae2_metrics_history.append(vqvae2_metrics) | |
| # Mantém apenas últimas 100 entries para limitar memória | |
| if len(self.vqvae2_metrics_history) > 100: | |
| self.vqvae2_metrics_history = self.vqvae2_metrics_history[-100:] | |
| return vqvae2_metrics | |
| except Exception as e: | |
| return { | |
| "active": False, | |
| "error": str(e)[:200], | |
| "vq_loss": 0.0, | |
| "recon_loss": 0.0, | |
| "total_loss": 0.0, | |
| } | |
| def get_vqvae2_metrics(self) -> Dict[str, Any]: | |
| """V6.5 — Retorna métricas atuais do VQ-VAE-2 compressor.""" | |
| if not self.enable_vqvae2 or self.vqvae2_compressor is None: | |
| return {"active": False, "reason": "disabled"} | |
| if not self.vqvae2_metrics_history: | |
| return {"active": True, "n_calls": 0} | |
| latest = self.vqvae2_metrics_history[-1] | |
| # V6.5: skip NaN values when computing means (early calls may produce NaN | |
| # due to Gumbel-softmax instability before codebook warmup) | |
| import math | |
| valid_total = [m.get("total_loss", 0.0) for m in self.vqvae2_metrics_history | |
| if not math.isnan(m.get("total_loss", 0.0))] | |
| valid_recon = [m.get("recon_loss", 0.0) for m in self.vqvae2_metrics_history | |
| if not math.isnan(m.get("recon_loss", 0.0))] | |
| return { | |
| "active": True, | |
| "n_calls": len(self.vqvae2_metrics_history), | |
| "latest": latest, | |
| "mean_total_loss": float(sum(valid_total) / max(1, len(valid_total))) if valid_total else 0.0, | |
| "mean_recon_loss": float(sum(valid_recon) / max(1, len(valid_recon))) if valid_recon else 0.0, | |
| "n_nan_skipped": len(self.vqvae2_metrics_history) - len(valid_total), | |
| } | |
| # ================================================================== | |
| # V6.5-final — SmoothQuant W8A8 compression of SOM weights | |
| # ================================================================== | |
| # User requirement: "integrar SmoothQuantCompressor ao KLS para | |
| # compressão W8A8 real dos pesos do SOM" | |
| # | |
| # Pipeline (executado periodicamente durante o treino): | |
| # 1. Coleta pesos atuais do SOM: W ∈ ℝ^(I,J,K,L,4) → reshape [N, 4] | |
| # 2. Coleta buffer_4d como amostras de ativação reais: X ∈ ℝ^(B, 4) | |
| # 3. Calibra SmoothQuant: s_j = max|X_j|^α / max|W_j|^(1-α) | |
| # 4. Aplica smoothing: W_smooth = W * diag(s) | |
| # 5. Quantiza INT8 simétrico per-tensor: W_int8 = round(W_smooth / scale) | |
| # 6. Dequantiza: W_dequant = W_int8 * scale / diag(s) (volta para float) | |
| # 7. Substitui self.som.weights pelo dequantizado (preserva topologia) | |
| # 8. Computa erro relativo: ||W - W_dequant|| / ||W|| | |
| # | |
| # Erro esperado: < 1% (SmoothQuant preserva acurácia). | |
| def compress_som_weights_w8a8(self) -> Dict[str, Any]: | |
| """V6.5-final — Aplica compressão W8A8 real aos pesos do SOM. | |
| SmoothQuant migra escala da ativação para o peso, permitindo | |
| quantização INT8 sem perda significativa. Os pesos são quantizados | |
| a INT8 e dequantizados de volta para float, preservando a | |
| topologia do SOM com erro relativo < 1%. | |
| User requirement: "compressão W8A8 real dos pesos do SOM" | |
| Returns: | |
| Dict com métricas de compressão (erro, ratio, scale stats). | |
| """ | |
| if not self.enable_w8a8 or self.w8a8_compressor is None: | |
| return {"active": False, "reason": "w8a8_disabled"} | |
| try: | |
| import time as _time | |
| t0 = _time.time() | |
| # 1. Snapshot dos pesos atuais do SOM | |
| W = self.som.weights # (I, J, K, L, 4) | |
| original_shape = W.shape | |
| N = W.shape[0] * W.shape[1] * W.shape[2] * W.shape[3] | |
| # Reshape para [N, 4] — trata cada neurônio como uma "linha" | |
| # e cada componente (x, y, z, w) como uma "feature" | |
| W_2d = W.reshape(N, 4).clone().detach() | |
| W_orig_norm = W_2d.norm().item() | |
| # 2. Calibra com buffer_4d (amostras de ativação reais) | |
| if len(self.buffer_4d) >= 4: | |
| X = torch.stack(self.buffer_4d[-self.w8a8_compressor.calibration_samples:]) | |
| X = X.detach().clone() | |
| else: | |
| # Fallback: usa os próprios pesos como amostras de ativação | |
| X = W_2d.detach().clone() | |
| # Sanitiza NaN/Inf | |
| W_2d = torch.nan_to_num(W_2d, nan=0.0, posinf=1e4, neginf=-1e4) | |
| X = torch.nan_to_num(X, nan=0.0, posinf=1e4, neginf=-1e4) | |
| # 3. Calibra SmoothQuant (computa smooth_scale s) | |
| self.w8a8_compressor.calibrate(W_2d, X) | |
| smooth_scale = self.w8a8_compressor.smooth_scale | |
| # 4. Aplica smoothing: W_smooth = W * diag(s) | |
| W_smooth = self.w8a8_compressor.smooth_weight(W_2d) | |
| # 5. Quantiza INT8 simétrico per-tensor | |
| W_int8 = self.w8a8_compressor.quantize_per_tensor_symmetric(W_smooth) | |
| # 6. Dequantiza: W_int8 * scale → W_smooth_recon, depois / diag(s) → W_dequant | |
| W_smooth_recon = self.w8a8_compressor.dequantize(W_int8, W_smooth) | |
| # Reverte smoothing: W_dequant = W_smooth_recon / diag(s) | |
| W_dequant = W_smooth_recon / smooth_scale.unsqueeze(0) | |
| # 7. Computa erro relativo | |
| error = (W_2d - W_dequant).norm().item() | |
| relative_error = error / max(W_orig_norm, 1e-8) | |
| # 8. Substitui pesos do SOM pelo dequantizado (preserva topologia) | |
| # IMPORTANTE: usa no_grad para não poluir gradiente (SOM não usa | |
| # autograd, mas mantemos consistência) | |
| with torch.no_grad(): | |
| self.som.weights = W_dequant.reshape(*original_shape).clone() | |
| # 9. Métricas | |
| compression_ratio = (W_2d.element_size() * W_2d.numel()) / (W_int8.element_size() * W_int8.numel()) | |
| metrics = { | |
| "active": True, | |
| "n_neurons": int(N), | |
| "n_features": 4, | |
| "n_calibration_samples": int(X.shape[0]), | |
| "smooth_scale_mean": float(smooth_scale.mean().item()), | |
| "smooth_scale_std": float(smooth_scale.std().item()), | |
| "smooth_scale_min": float(smooth_scale.min().item()), | |
| "smooth_scale_max": float(smooth_scale.max().item()), | |
| "alpha": float(self.w8a8_compressor.alpha), | |
| "n_bits": int(self.w8a8_compressor.n_bits), | |
| "compression_ratio": float(compression_ratio), | |
| "weights_norm_original": float(W_orig_norm), | |
| "weights_norm_dequant": float(W_dequant.norm().item()), | |
| "absolute_error": float(error), | |
| "relative_error": float(relative_error), | |
| "preserves_topology": bool(relative_error < 0.05), # < 5% = OK | |
| "w8a8_int8_range": [ | |
| int(W_int8.min().item()), | |
| int(W_int8.max().item()), | |
| ], | |
| "elapsed_ms": float((_time.time() - t0) * 1000), | |
| } | |
| self.w8a8_metrics_history.append(metrics) | |
| # Mantém apenas últimas 100 entries | |
| if len(self.w8a8_metrics_history) > 100: | |
| self.w8a8_metrics_history = self.w8a8_metrics_history[-100:] | |
| return metrics | |
| except Exception as e: | |
| return { | |
| "active": False, | |
| "error": str(e)[:200], | |
| "preserves_topology": False, | |
| } | |
| def get_w8a8_metrics(self) -> Dict[str, Any]: | |
| """V6.5-final — Retorna métricas atuais do compressor W8A8.""" | |
| if not self.enable_w8a8 or self.w8a8_compressor is None: | |
| return {"active": False, "reason": "disabled"} | |
| if not self.w8a8_metrics_history: | |
| return {"active": True, "n_calls": 0} | |
| latest = self.w8a8_metrics_history[-1] | |
| all_errors = [m.get("relative_error", 0.0) for m in self.w8a8_metrics_history] | |
| return { | |
| "active": True, | |
| "n_calls": len(self.w8a8_metrics_history), | |
| "latest": latest, | |
| "mean_relative_error": float(sum(all_errors) / max(1, len(all_errors))), | |
| "max_relative_error": float(max(all_errors)) if all_errors else 0.0, | |
| "all_preserve_topology": all(m.get("preserves_topology", False) for m in self.w8a8_metrics_history), | |
| } | |
| # ------------------------------------------------------------------ | |
| # V6.5 — ReasoningEngine integration | |
| # ------------------------------------------------------------------ | |
| def reason_about(self, query: str) -> Iterator[str]: | |
| """V6.5 — Gera streaming de raciocínio para uma query. | |
| Usa o ReasoningEngine integrado para produzir tags <think>, <plan>, | |
| <decompose>, <execute>, <monitor>, <predict>, <adjust>, <answer>. | |
| Compatível com Ollama/LangChain/vLLM (tags padrão). | |
| Args: | |
| query: pergunta/requisição do usuário. | |
| Yields: | |
| chunks de texto (tags + conteúdo). | |
| """ | |
| if not self.enable_reasoning or self.reasoning_engine is None: | |
| yield f"<answer>ReasoningEngine disabled. SOM prediction: {self.predict(query)}</answer>" | |
| return | |
| yield from self.reasoning_engine.solve(query, use_tools=True, use_planning=True) | |
| def reason_sync(self, query: str) -> str: | |
| """V6.5 — Versão síncrona de reason_about (retorna string completa).""" | |
| return "".join(self.reason_about(query)) | |
| def get_reasoning_stats(self) -> Dict[str, Any]: | |
| """V6.5 — Retorna estatísticas do reasoning_engine.""" | |
| if not self.enable_reasoning or self.reasoning_engine is None: | |
| return {"active": False, "reason": "disabled"} | |
| return { | |
| "active": True, | |
| "stats": self.reasoning_engine.get_stats(), | |
| "n_history": len(self.reasoning_engine.history), | |
| } | |
| def _som_activation(self, x): | |
| """Vetor de ativação SOM: distâncias de x a todos os neurônios (flatten).""" | |
| dist = torch.sum((self.som.weights - x.view(1, 1, 1, 1, 4)) ** 2, dim=-1) | |
| return dist.flatten() | |
| def _label_neurons(self): | |
| """Rotula neurônios por votação majoritária sobre o buffer. | |
| V6.5-V2-memfix — no_grad to prevent graph building during BMU search. | |
| """ | |
| self.neuron_label = {} | |
| if not self.buffer_4d: | |
| return | |
| data = torch.stack(self.buffer_4d).detach() | |
| labels = torch.tensor(self.buffer_labels) | |
| votes = defaultdict(lambda: [0, 0]) | |
| with torch.no_grad(): | |
| for i in range(len(data)): | |
| bmu = self.som.find_bmu(data[i]) | |
| votes[bmu][int(labels[i].item())] += 1 | |
| for bmu, v in votes.items(): | |
| self.neuron_label[bmu] = 1.0 if v[1] > v[0] else 0.0 | |
| def evaluate_classification(self) -> float: | |
| """Acurácia sobre o buffer atual. | |
| V6.5-V2-memfix — no_grad to prevent graph building during evaluation. | |
| """ | |
| if not self.buffer_4d: | |
| return 1.0 | |
| data = torch.stack(self.buffer_4d).detach() | |
| labels = torch.tensor(self.buffer_labels).float() | |
| correct = 0 | |
| with torch.no_grad(): | |
| for i in range(len(data)): | |
| pred = self._predict_single(data[i]) | |
| if (pred > 0.5) == (labels[i] > 0.5): | |
| correct += 1 | |
| return correct / len(data) | |
| def _predict_single(self, x): | |
| """Prediz: classifier (se treinado) ou voto do BMU.""" | |
| if self.classifier is not None and self.classifier_trained: | |
| with torch.no_grad(): | |
| act = self._som_activation(x).unsqueeze(0) | |
| logit = self.classifier(act) | |
| return torch.sigmoid(logit).item() | |
| else: | |
| if not hasattr(self, "neuron_label"): | |
| self._label_neurons() | |
| bmu = self.som.find_bmu(x) | |
| return self.neuron_label.get(bmu, 0.5) | |
| def activate_hypothesis(self): | |
| """Treina o HypothesisClassifier (8 FC layers) por 50 epochs. | |
| BUG FIX (V6.3→V6.4): buffer_4d contém tensores que carregam o grafo | |
| de computação do embedding. Para evitar "Trying to backward through | |
| the graph a second time", fazemos detach+clone e calculamos as | |
| ativações SOM dentro de torch.no_grad(). O classifier treina apenas | |
| sobre seus próprios pesos. | |
| """ | |
| if self.classifier is None: | |
| self.classifier = HypothesisClassifier( | |
| self.som_neuron_count, self.hypothesis_hidden | |
| ) | |
| # FIX: detach+clone para isolar do grafo do embedding | |
| data = torch.stack(self.buffer_4d).detach().clone() | |
| labels = torch.tensor(self.buffer_labels).float() | |
| # FIX: ativações SEM gradiente (não queremos treinar SOM/embedding aqui) | |
| with torch.no_grad(): | |
| X = torch.stack([self._som_activation(data[i]) for i in range(len(data))]) | |
| optimizer = torch.optim.Adam(self.classifier.parameters(), lr=0.001) | |
| criterion = nn.BCEWithLogitsLoss() | |
| for _ in range(50): | |
| optimizer.zero_grad() | |
| loss = criterion(self.classifier(X), labels) | |
| loss.backward() | |
| optimizer.step() | |
| self.classifier_trained = True | |
| def process_batch( | |
| self, | |
| sentences: List[str], | |
| labels: List[int], | |
| label_strings: Optional[Dict[int, str]] = None, | |
| dataset_name: Optional[str] = None, | |
| ): | |
| """V6.5-fix — Processa batch: adiciona dados, treina SOM se ready, aplica punishment. | |
| Args: | |
| sentences: lista de textos. | |
| labels: lista de inteiros (0/1). | |
| label_strings: opcional — {0: 'str0', 1: 'str1'} para o dataset. | |
| dataset_name: opcional — nome do dataset (registrado no label_registry). | |
| Returns: | |
| True se o protocolo de punishment completou um ciclo (2ª punição | |
| → set_ewc_reference + reset). False caso contrário. | |
| """ | |
| # V6.5-fix: forward label_strings + dataset_name to add_data so | |
| # the label_registry is populated for predict() lookups. | |
| self.add_data( | |
| sentences, labels, | |
| label_strings=label_strings, | |
| dataset_name=dataset_name, | |
| ) | |
| if self.check_training_start(): | |
| self.train_som_on_buffer() | |
| self._label_neurons() | |
| if self.training_ready: | |
| acc = self.evaluate_classification() | |
| if acc < 1.0: | |
| self.punishment_count += 1 | |
| self.success_count = 0 | |
| if self.punishment_count == 1: | |
| self.activate_hypothesis() | |
| elif self.punishment_count == 2: | |
| self.som.set_ewc_reference() | |
| self.required_new_samples = ( | |
| self.success_count * self.N | |
| if self.success_count > 0 | |
| else self.N | |
| ) | |
| self.training_ready = False | |
| self.punishment_count = 0 | |
| self.success_count = 0 | |
| self.histogram.clear() | |
| self.buffer_4d.clear() | |
| self.buffer_labels.clear() | |
| return True | |
| else: | |
| self.punishment_count = 0 | |
| self.success_count += 1 | |
| return False | |
| def predict( | |
| self, | |
| sentence: str, | |
| dataset_name: Optional[str] = None, | |
| return_proba: bool = False, | |
| ) -> Any: | |
| """V6.5-fix — Prediz rótulo textual DINÂMICO extraído do label_registry. | |
| User requirement (CORREÇÃO MATEMÁTICA): | |
| "os strings 'gato' e 'cachorro' são fixos quando deveriam ser | |
| extrações variáveis e flexíveis de rótulos proveniente de dados | |
| dos datasets anteriormente treinados" | |
| Mathematical correction: | |
| ANTES (BUG): | |
| prob = _predict_single(vec_4d) | |
| return "gato" if prob <= 0.5 else "cachorro" # FIXO! | |
| AGORA (V6.5-fix): | |
| 1. vec_4d = text_to_4d_vector(sentence, ...) | |
| 2. prob = _predict_single(vec_4d) ∈ [0, 1] | |
| (sigmoid(logit) se classifier treinado; voto BMU caso contrário) | |
| 3. label_int = 1 if prob > 0.5 else 0 | |
| 4. label_str = self.get_label_string(label_int, dataset_name) | |
| - consulta label_registry[dataset_name] se dataset_name fornecido | |
| - fallback: último dataset registrado | |
| - fallback: default {0:'negative', 1:'positive'} | |
| 5. return label_str [ou (label_str, prob) se return_proba=True] | |
| V6.5-V2-metrics-FIX-2: Wrapped in torch.no_grad() to prevent computation | |
| graph building during inference. Without this, each predict() call | |
| builds a graph that includes the embedding (16M params), attention | |
| forward pass, and SVD — consuming ~100MB+ per call. With 4 parallel | |
| tool_coordinator workers calling predict(), this caused OOM (3.5GB RSS). | |
| Args: | |
| sentence: texto de entrada. | |
| dataset_name: opcional — nome do dataset para disambiguar rótulos. | |
| return_proba: se True, retorna (label_str, prob). | |
| Returns: | |
| str (ou Tuple[str, float]) — rótulo dinâmico extraído dos datasets. | |
| """ | |
| self.time_counter += 1 # mantém coerência temporal | |
| # V6.5-V2-metrics-FIX-2 — no_grad para evitar building de grafo em inference | |
| with torch.no_grad(): | |
| vec = text_to_4d_vector( | |
| sentence, | |
| self.tokenizer, | |
| self.embedding, | |
| self.hidden_dim, | |
| self.seq_len, | |
| self.time_counter, | |
| self.T_max, | |
| attention=self.attention, # V6.5-attn | |
| attention_metrics=self.attention_metrics, # V6.5-attn | |
| ) | |
| prob = self._predict_single(vec) | |
| # CORREÇÃO MATEMÁTICA: label_int derivado de prob, não hardcoded | |
| label_int = 1 if prob > 0.5 else 0 | |
| # Rótulo textual DINÂMICO extraído do label_registry | |
| label_str = self.get_label_string(label_int, dataset_name) | |
| if return_proba: | |
| return label_str, float(prob) | |
| return label_str | |
| def predict_proba( | |
| self, | |
| sentence: str, | |
| dataset_name: Optional[str] = None, | |
| ) -> Tuple[str, float]: | |
| """V6.5-fix — Prediz (label_str, prob) — conveniência para inferência.""" | |
| return self.predict(sentence, dataset_name=dataset_name, return_proba=True) | |
| # ================================================================== | |
| # V6.5-fix — Inference-time Punishment via Hypothesis Layer | |
| # ================================================================== | |
| # User requirement: "ao punir o modelo reajusta seus parâmetros usando | |
| # a camada de hipótese para funcionar como na lógica de punição e | |
| # ajuste de parâmetros durante o treinamento" | |
| # | |
| # Mathematical principle (mirrors training punishment protocol): | |
| # | |
| # TRAINING protocol (process_batch): | |
| # acc = evaluate_classification(buffer) | |
| # if acc < 1.0: | |
| # punishment_count += 1 | |
| # success_count = 0 | |
| # if punishment_count == 1: | |
| # activate_hypothesis() # treina HypothesisClassifier | |
| # elif punishment_count == 2: | |
| # set_ewc_reference() # consolida w via Fisher | |
| # reset cycle | |
| # | |
| # INFERENCE protocol (punish_during_inference) — V6.5-fix: | |
| # When external feedback indicates the predicted label was WRONG: | |
| # 1. Compute vec_4d for the sentence. | |
| # 2. Add (sentence, correct_label_int) to buffer_4d + buffer_labels. | |
| # 3. Update SOM via Kohonen rule: ΔW = α·Λ·(x - W) on BMU. | |
| # 4. inference_punishment_count += 1 | |
| # punishment_count += 1 | |
| # success_count = 0 | |
| # 5. If punishment_count == 1: | |
| # activate_hypothesis() — re-treina HypothesisClassifier | |
| # usando o buffer expandido (Adam + BCEWithLogitsLoss, 50 epochs). | |
| # Ajusta pesos do classifier: θ_{t+1} = θ_t - η·∂L/∂θ. | |
| # If punishment_count >= 2: | |
| # set_ewc_reference() — consolida w via Fisher information: | |
| # F_i = mean((x_w - W_w,i)²) | |
| # old_weights_w = W_w.clone() | |
| # Reset cycle (mirror training). | |
| # 6. Return outcome dict with action taken + accuracy_after. | |
| # | |
| # This ensures that during inference, the model can self-correct using | |
| # the same hypothesis-layer-based parameter adjustment mechanism used | |
| # in training — NOT a separate inference-only correction. | |
| # ------------------------------------------------------------------ | |
| def punish_during_inference( | |
| self, | |
| sentence: str, | |
| correct_label_int: int, | |
| correct_label_str: Optional[str] = None, | |
| dataset_name: Optional[str] = None, | |
| ) -> Dict[str, Any]: | |
| """V6.5-fix — Punição na inferência via camada de hipótese. | |
| Mirror do protocolo de punição de treinamento, aplicado em inference | |
| quando o usuário (ou avaliador) fornece feedback de que a predição | |
| foi incorreta. | |
| Args: | |
| sentence: texto que foi mal-classificado. | |
| correct_label_int: rótulo inteiro correto (0 ou 1). | |
| correct_label_str: opcional — rótulo textual correto. | |
| dataset_name: opcional — nome do dataset para registrar label. | |
| Returns: | |
| Dict com: | |
| - action: 'activate_hypothesis' | 'set_ewc_reference' | 'none' | |
| - inference_punishment_count: total de punições em inference | |
| - punishment_count_cycle: contador no ciclo atual (0, 1, ou 2+) | |
| - classifier_trained: bool | |
| - ewc_reference_set: bool | |
| - accuracy_after_punishment: float | |
| - buffer_size: int | |
| - elapsed_ms: float | |
| """ | |
| import time as _time | |
| t0 = _time.time() | |
| # 1. Register label string if provided | |
| if correct_label_str is not None and dataset_name is not None: | |
| if dataset_name not in self.label_registry: | |
| self.label_registry[dataset_name] = {} | |
| self.label_registry[dataset_name][int(correct_label_int)] = str(correct_label_str) | |
| self.last_dataset_used = dataset_name | |
| # 2. Compute vec_4d for the misclassified sentence | |
| # V6.5-V2-memfix — no_grad + detach to prevent graph accumulation | |
| with torch.no_grad(): | |
| self.time_counter += 1 | |
| vec = text_to_4d_vector( | |
| sentence, | |
| self.tokenizer, | |
| self.embedding, | |
| self.hidden_dim, | |
| self.seq_len, | |
| self.time_counter, | |
| self.T_max, | |
| attention=self.attention, | |
| attention_metrics=self.attention_metrics, | |
| ) | |
| vec = vec.detach().clone() | |
| # 3. Add to buffer + update histogram (mirror add_data) | |
| self.buffer_4d.append(vec) | |
| self.buffer_labels.append(int(correct_label_int)) | |
| # Enforce sliding window limit (mirror add_data) | |
| if len(self.buffer_4d) > self.buffer_max_size: | |
| self.buffer_4d = self.buffer_4d[-self.buffer_max_size:] | |
| self.buffer_labels = self.buffer_labels[-self.buffer_max_size:] | |
| dim_val = round(vec[self.dim_index].item(), 2) | |
| self.histogram[dim_val] += 1 | |
| # 4. Force SOM update on the new sample (Kohonen rule) | |
| # ΔW = α·Λ·(x - W) [plus EWC penalty if reference set] | |
| bmu = self.som.find_bmu(vec) | |
| acc_fisher = ( | |
| self.punishment_count == 0 | |
| and self.som.old_weights_w is None | |
| ) | |
| try: | |
| self.som.update_weights(vec, bmu, accumulate_fisher=acc_fisher) | |
| except Exception: | |
| pass # non-fatal: SOM update may fail in edge cases | |
| # 5. Punishment protocol (mirror training process_batch) | |
| self.inference_punishment_count += 1 | |
| self.punishment_count += 1 | |
| self.success_count = 0 | |
| action = "none" | |
| classifier_trained = bool(self.classifier_trained) | |
| ewc_set = self.som.old_weights_w is not None | |
| if self.punishment_count == 1: | |
| # 1st punishment: re-train HypothesisClassifier | |
| # Math: θ_{t+1} = θ_t - η·∂L_BCE/∂θ, 50 epochs, Adam (η=0.001) | |
| try: | |
| self.activate_hypothesis() | |
| action = "activate_hypothesis" | |
| classifier_trained = True | |
| except Exception as e: | |
| action = f"activate_hypothesis_failed: {str(e)[:200]}" | |
| elif self.punishment_count >= 2: | |
| # 2nd punishment: set EWC reference + reset cycle | |
| # Math: F_i = mean((x_w - W_w,i)²); old_W_w = W_w.clone() | |
| try: | |
| self.som.set_ewc_reference() | |
| ewc_set = True | |
| action = "set_ewc_reference" | |
| except Exception as e: | |
| action = f"set_ewc_reference_failed: {str(e)[:200]}" | |
| # Reset cycle (mirror training) | |
| self.training_ready = False | |
| self.punishment_count = 0 | |
| self.success_count = 0 | |
| self.histogram.clear() | |
| # NOTE: do NOT fully clear buffer during inference — keep a | |
| # sliding window of last 256 samples to preserve recent feedback | |
| # while bounding memory. | |
| if len(self.buffer_4d) > 256: | |
| self.buffer_4d = self.buffer_4d[-256:] | |
| self.buffer_labels = self.buffer_labels[-256:] | |
| # 6. Re-evaluate classification accuracy after punishment | |
| try: | |
| acc_after = self.evaluate_classification() | |
| except Exception: | |
| acc_after = 0.0 | |
| outcome = { | |
| "action": action, | |
| "inference_punishment_count": int(self.inference_punishment_count), | |
| "punishment_count_cycle": int(self.punishment_count), | |
| "classifier_trained": bool(classifier_trained), | |
| "ewc_reference_set": bool(ewc_set), | |
| "accuracy_after_punishment": float(acc_after), | |
| "buffer_size": int(len(self.buffer_4d)), | |
| "bmu_of_misclassified": tuple(int(x) for x in bmu) if bmu else None, | |
| "elapsed_ms": float((_time.time() - t0) * 1000), | |
| } | |
| self.inference_punishment_history.append(outcome) | |
| if len(self.inference_punishment_history) > 100: | |
| self.inference_punishment_history = self.inference_punishment_history[-100:] | |
| return outcome | |
| def get_inference_punishment_history(self) -> List[Dict[str, Any]]: | |
| """V6.5-fix — Retorna histórico de punições em inference.""" | |
| return list(self.inference_punishment_history) | |
| # ------------------------------------------------------------------ | |
| # API de monitoramento (V6.4 + V6.5) | |
| # ------------------------------------------------------------------ | |
| def get_state_metrics(self) -> dict: | |
| """Retorna métricas completas do sistema para monitoramento. | |
| V6.5: inclui vqvae2_metrics e reasoning_metrics. | |
| V6.5-final: inclui w8a8_metrics e tool_coordinator_workers. | |
| """ | |
| som_metrics = self.som.get_metrics() | |
| return { | |
| "som": som_metrics, | |
| "kls": { | |
| "time_counter": int(self.time_counter), | |
| "T_max": int(self.T_max), | |
| "buffer_size": int(len(self.buffer_4d)), | |
| "training_ready": bool(self.training_ready), | |
| "punishment_count": int(self.punishment_count), | |
| "success_count": int(self.success_count), | |
| "classifier_trained": bool(self.classifier_trained), | |
| "histogram_size": int(len(self.histogram)), | |
| "histogram_max": int(max(self.histogram.values(), default=0)), | |
| "N_start": int(self.N), | |
| "dim_choice": str(self.dim_choice), | |
| "som_neuron_count": int(self.som_neuron_count), | |
| "required_new_samples": int(self.required_new_samples), | |
| "has_classifier": self.classifier is not None, | |
| "vocab_size": int(self.tokenizer.vocab_size), | |
| "hidden_dim": int(self.hidden_dim), | |
| "seq_len": int(self.seq_len), | |
| # V6.5 | |
| "enable_vqvae2": bool(self.enable_vqvae2), | |
| "enable_reasoning": bool(self.enable_reasoning), | |
| "enable_w8a8": bool(self.enable_w8a8), | |
| "vqvae2_n_calls": int(len(self.vqvae2_metrics_history)), | |
| "w8a8_n_calls": int(len(self.w8a8_metrics_history)), | |
| # V6.5-final: tool_coordinator workers reativados | |
| "tool_coordinator_workers_active": bool( | |
| getattr(self, "_tool_coordinator_workers_active", False) | |
| ), | |
| # V6.5-attn: attention module integrado | |
| "enable_attention": bool(getattr(self, "enable_attention", False)), | |
| "attention_active": bool(self.attention is not None), | |
| "attention_n_heads": int(getattr(self, "attention_n_heads", 0)), | |
| # V6.5-fix: dynamic label registry + inference punishment | |
| "label_registry_n_datasets": int(len(self.label_registry)), | |
| "last_dataset_used": str(self.last_dataset_used) if self.last_dataset_used else None, | |
| "inference_punishment_count": int(self.inference_punishment_count), | |
| "inference_punishment_history_len": int(len(self.inference_punishment_history)), | |
| }, | |
| # V6.5 — VQ-VAE-2 metrics | |
| "vqvae2": self.get_vqvae2_metrics(), | |
| # V6.5 — ReasoningEngine metrics | |
| "reasoning": self.get_reasoning_stats(), | |
| # V6.5-attn — MultiHeadAttention metrics | |
| "attention": self.get_attention_metrics(), | |
| # V6.5-final — SmoothQuant W8A8 metrics | |
| "w8a8": self.get_w8a8_metrics(), | |
| # V6.5-fix — Dynamic label registry state | |
| "label_registry": self.get_label_registry_state(), | |
| } | |
| # ============================================================================ | |
| # V6.5-V2 — HypothesisEnsemble (16 hipóteses) + KohonenLearningSystemV2 | |
| # ============================================================================ | |
| # User requirement: "Formato de Aprendizado com 16 Hipóteses e 3 Tentativas" | |
| # | |
| # Fluxo: | |
| # 1. Fase sem punição (streaming N-datasets) de CONHECIMENTO: | |
| # 1.1. Acumulação de conhecimento → ajuste dos pesos do SOM via regra | |
| # de Kohonen. | |
| # 1.2. Construção do MAPA-SOM (estado consolidado) com treinamento | |
| # contínuo. | |
| # | |
| # 2. Fase com punição (streaming M-datasets): | |
| # 2.1. Cada evento de erro (punição) dispara 3 tentativas de ajuste. | |
| # 2.2. Em cada tentativa, 16 geradores paralelos de hipóteses produzem | |
| # candidatos a delta. | |
| # 2.3. O melhor delta (que maximiza a acurácia no buffer) é aplicado | |
| # ao MAPA-SOM. | |
| # 2.4. Após a aplicação, consolidação via EWC (para evitar esquecimento | |
| # catastrófico). | |
| # 2.5. O ciclo é reiniciado sem loops infinitos (número fixo de | |
| # tentativas). | |
| # | |
| # Mathematical analysis: | |
| # - DeltaGenerator: g_h(x) → δ_h ∈ ℝ^P (h = 1..16) | |
| # onde P = I*J*K*L*4 (parâmetros do SOM achatados) | |
| # - HypothesisEnsemble: H = {g_1, ..., g_16} | |
| # - Avaliação: a_h = accuracy(SOM + δ_h, buffer) para cada h | |
| # - Seleção: δ* = argmax_h a_h | |
| # - Aplicação: SOM ← SOM + δ* | |
| # - Consolidação: set_ewc_reference() (Fisher information em w) | |
| # | |
| # Treinamento dos geradores (train_hypotheses): | |
| # - Para cada punição, executar hyp_train_steps passos de Adam | |
| # - Loss = mean_h(BCEWithLogitsLoss(classifier(SOM+δ_h), labels)) | |
| # - δ_h = scale * g_h(som_activation_mean) | |
| # - scale é um parâmetro treinável (começa em 0.01, clamp [0.001, 0.1]) | |
| # ============================================================================ | |
| class DeltaGenerator(nn.Module): | |
| """Gerador de delta para ajuste do MAPA-SOM. | |
| Entrada: vetor de ativação SOM (distâncias ao grid) ou vetor 4D médio. | |
| Saída: delta com a mesma dimensionalidade do SOM achatado. | |
| Arquitetura: Linear → ReLU → Linear → ReLU → Linear (Xavier init gain=0.01) | |
| """ | |
| def __init__(self, input_dim: int, output_dim: int, hidden_dim: int = 256): | |
| super().__init__() | |
| self.net = nn.Sequential( | |
| nn.Linear(input_dim, hidden_dim), | |
| nn.ReLU(), | |
| nn.Linear(hidden_dim, hidden_dim), | |
| nn.ReLU(), | |
| nn.Linear(hidden_dim, output_dim), | |
| ) | |
| # Inicialização Xavier para garantir deltas pequenos no início | |
| for layer in self.net: | |
| if isinstance(layer, nn.Linear): | |
| nn.init.xavier_uniform_(layer.weight, gain=0.01) | |
| nn.init.zeros_(layer.bias) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| return self.net(x) | |
| class HypothesisEnsemble(nn.Module): | |
| """Conjunto de N geradores de hipóteses paralelos (com active_count dinâmico). | |
| V6.5-V2-dynamic: pré-aloca max_n_hypotheses geradores, mas usa apenas | |
| os primeiros `active_count` em forward/forward_stacked. Isto permite | |
| ajustar dinamicamente o número de hipóteses ativas sem recriar o módulo. | |
| Args: | |
| input_dim: dimensionalidade do vetor de ativação SOM (som_neuron_count). | |
| output_dim: número de parâmetros do SOM achatados (I*J*K*L*4). | |
| n_hypotheses: número ATIVO inicial de geradores (default 16). | |
| max_n_hypotheses: número MÁXIMO pré-alocado (default 32). | |
| hidden_dim: largura das camadas internas de cada gerador. | |
| """ | |
| def __init__( | |
| self, | |
| input_dim: int, | |
| output_dim: int, | |
| n_hypotheses: int = 16, | |
| max_n_hypotheses: int = 32, | |
| hidden_dim: int = 256, | |
| ): | |
| super().__init__() | |
| # Garante que n_hypotheses <= max_n_hypotheses | |
| n_hypotheses = max(1, min(n_hypotheses, max_n_hypotheses)) | |
| self.n_hypotheses = n_hypotheses # compat (== active_count inicial) | |
| self.max_n_hypotheses = max_n_hypotheses | |
| self.active_count = n_hypotheses | |
| # Pré-aloca max_n_hypotheses geradores (mesmo os inativos terão params | |
| # inicializados — mas não receberão gradiente até serem ativados, pois | |
| # forward_stacked itera apenas até active_count). | |
| self.generators = nn.ModuleList( | |
| [DeltaGenerator(input_dim, output_dim, hidden_dim) | |
| for _ in range(max_n_hypotheses)] | |
| ) | |
| def set_active_count(self, n: int) -> None: | |
| """Ajusta dinamicamente o número de geradores ativos.""" | |
| n = max(1, min(int(n), self.max_n_hypotheses)) | |
| self.active_count = n | |
| self.n_hypotheses = n # mantém compat com código legado | |
| def forward(self, x: torch.Tensor) -> List[torch.Tensor]: | |
| """Retorna lista de `active_count` deltas (cada um (batch, output_dim)).""" | |
| return [self.generators[i](x) for i in range(self.active_count)] | |
| def forward_stacked(self, x: torch.Tensor) -> torch.Tensor: | |
| """Retorna tensor (batch, active_count, output_dim) para vetorização.""" | |
| return torch.stack( | |
| [self.generators[i](x) for i in range(self.active_count)], dim=1 | |
| ) | |
| class KohonenLearningSystemV2(KohonenLearningSystem): | |
| """Versão V2 do KLS com hipóteses dinâmicas e tentativas adaptativas. | |
| Herda toda a infraestrutura do KLS V6.5 (SOM 4D, EWC, VQ-VAE-2, W8A8, | |
| attention, reasoning_engine, label_registry dinâmico) e adiciona: | |
| a. HypothesisEnsemble: até max_n_hypotheses geradores de delta (default 32, | |
| com n_hypotheses ativos = 16 inicialmente). | |
| b. select_best_delta: avalia n_trials tentativas × n_hypotheses hipóteses. | |
| c. apply_best_delta_and_consolidate: aplica o delta e chama set_ewc_reference. | |
| d. train_hypotheses: treina os geradores via Adam + BCE loss. | |
| e. DynamicHyperparameterController: ajusta automaticamente n_hypotheses, | |
| n_trials e hyp_train_steps com base em: | |
| - histórico de loss (slope + volatilidade) | |
| - recorrência de punições (taxa nos últimos K batches) | |
| O protocolo de punição é estendido: | |
| - punishment_count == 1 → treinar hipóteses (train_hypotheses) + | |
| activate_hypothesis (classifier) | |
| - punishment_count == 2 → selecionar melhor delta (n_trials × | |
| n_hypotheses) + aplicar + set_ewc_reference + reset | |
| V6.5-V2-dynamic: | |
| Após cada train_hypotheses() e após cada evento de punição, o | |
| _adapt_hyperparameters() é chamado para re-ajustar (n_hypotheses, | |
| n_trials, hyp_train_steps) conforme a perda e a recorrência de punições. | |
| """ | |
| def __init__(self, *args, **kwargs): | |
| # Extrai parâmetros V2 antes de passar ao pai | |
| self.n_hypotheses = kwargs.pop("n_hypotheses", 16) | |
| self.max_n_hypotheses = kwargs.pop("max_n_hypotheses", 32) | |
| # Garante coerência | |
| self.n_hypotheses = max(1, min(self.n_hypotheses, self.max_n_hypotheses)) | |
| self.n_trials = kwargs.pop("n_trials", 3) | |
| self.hyp_train_steps = kwargs.pop("hyp_train_steps", 30) | |
| self.hyp_lr = kwargs.pop("hyp_lr", 1e-4) | |
| self.hyp_hidden_dim = kwargs.pop("hyp_hidden_dim", 256) | |
| # V6.5-V2-dynamic — limites para adaptação automática | |
| self.min_n_hypotheses = kwargs.pop("min_n_hypotheses", 4) | |
| self.max_n_trials = kwargs.pop("max_n_trials", 6) | |
| self.min_n_trials = kwargs.pop("min_n_trials", 1) | |
| self.max_hyp_train_steps = kwargs.pop("max_hyp_train_steps", 80) | |
| self.min_hyp_train_steps = kwargs.pop("min_hyp_train_steps", 10) | |
| # Janela de observação | |
| self.loss_history_window = kwargs.pop("loss_history_window", 8) | |
| self.punishment_window = kwargs.pop("punishment_window", 12) | |
| super().__init__(*args, **kwargs) | |
| # Inicialização do ensemble de hipóteses | |
| self._init_hypothesis_ensemble() | |
| # V6.5-V2-dynamic — estado do controlador adaptativo | |
| self._loss_history: List[float] = [] # últimos loss_final de train_hypotheses | |
| self._punishment_history: List[int] = [] # 1 se houve punição no batch, 0 caso contrário | |
| self._adaptation_log: List[Dict[str, Any]] = [] | |
| self._last_adaptation_step = 0 | |
| # V6.5-V2-metrics-FIX-2 — contadores cumulativos para monitoramento | |
| # User requirement: "quantos passos de treino de hipóteses usado" | |
| self._total_hyp_steps_executed: int = 0 | |
| self._n_train_hyp_calls: int = 0 | |
| # V6.5-V2-metrics — histórico de métricas SOM (QE, TE, KL, VE, Dead Neurons) | |
| # para detecção de Estagnação do QE e Cruzamento de Vizinhança. | |
| from bigru_t.model.som_metrics import SOMMetricHistory | |
| self._som_metric_history: SOMMetricHistory = SOMMetricHistory() | |
| def _init_hypothesis_ensemble(self): | |
| """Cria o ensemble de geradores + otimizador Adam.""" | |
| input_dim = self.som_neuron_count # ativação SOM flatten (256 para grid (4,4,4,4)) | |
| output_dim = self.som.weights.numel() # I*J*K*L*4 (256*4 = 1024) | |
| self.hypothesis_ensemble = HypothesisEnsemble( | |
| input_dim=input_dim, | |
| output_dim=output_dim, | |
| n_hypotheses=self.n_hypotheses, | |
| max_n_hypotheses=self.max_n_hypotheses, | |
| hidden_dim=self.hyp_hidden_dim, | |
| ) | |
| self.hyp_optimizer = torch.optim.Adam( | |
| self.hypothesis_ensemble.parameters(), lr=self.hyp_lr | |
| ) | |
| # delta_scale: parâmetro treinável para controlar magnitude dos deltas | |
| # começa pequeno (0.01) para evitar explosão no início | |
| self.delta_scale = nn.Parameter(torch.tensor(0.01)) | |
| # ================================================================== | |
| # V6.5-V2-dynamic — Controlador adaptativo de hiperparâmetros | |
| # ================================================================== | |
| def _record_loss(self, loss_final: float) -> None: | |
| """Adiciona um loss_final ao histórico (janela deslizante).""" | |
| self._loss_history.append(float(loss_final)) | |
| if len(self._loss_history) > self.loss_history_window: | |
| self._loss_history = self._loss_history[-self.loss_history_window:] | |
| def _record_punishment_event(self, had_punishment: bool) -> None: | |
| """Registra se houve punição no batch atual (janela deslizante).""" | |
| self._punishment_history.append(1 if had_punishment else 0) | |
| if len(self._punishment_history) > self.punishment_window: | |
| self._punishment_history = self._punishment_history[-self.punishment_window:] | |
| def _compute_loss_stats(self) -> Dict[str, float]: | |
| """Calcula slope e volatilidade do histórico de loss. | |
| Returns: | |
| {"slope": float, "volatility": float, "mean": float, "n": int} | |
| """ | |
| n = len(self._loss_history) | |
| if n < 2: | |
| return {"slope": 0.0, "volatility": 0.0, "mean": 0.0, "n": n} | |
| losses = list(self._loss_history) | |
| mean_loss = sum(losses) / n | |
| # Slope por mínimos quadrados: y = a*x + b, x = [0..n-1] | |
| x_mean = (n - 1) / 2.0 | |
| y_mean = mean_loss | |
| num = sum((i - x_mean) * (losses[i] - y_mean) for i in range(n)) | |
| den = sum((i - x_mean) ** 2 for i in range(n)) | |
| slope = num / den if den > 0 else 0.0 | |
| # Volatilidade = std / |mean| (CV) | |
| variance = sum((l - y_mean) ** 2 for l in losses) / n | |
| std = variance ** 0.5 | |
| volatility = std / max(abs(mean_loss), 1e-8) | |
| return { | |
| "slope": float(slope), | |
| "volatility": float(volatility), | |
| "mean": float(mean_loss), | |
| "std": float(std), | |
| "n": int(n), | |
| } | |
| def _compute_punishment_rate(self) -> float: | |
| """Calcula a taxa de punição na janela observada.""" | |
| if not self._punishment_history: | |
| return 0.0 | |
| return float(sum(self._punishment_history) / len(self._punishment_history)) | |
| def _adapt_hyperparameters(self, trigger: str = "auto") -> Dict[str, Any]: | |
| """V6.5-V2-dynamic — Ajusta n_hypotheses, n_trials, hyp_train_steps. | |
| Regras (matematicamente justificadas): | |
| - loss_volatility > 0.30 e n_hyp < max → n_hyp += 2 (mais diversidade) | |
| - loss_volatility < 0.05 e n_hyp > min → n_hyp -= 2 (menos desperdício) | |
| - loss_slope > -0.001 (lento/estagnado) e steps < max → steps += 5 | |
| - loss_slope < -0.01 (rápido) e steps > min → steps -= 5 | |
| - punishment_rate > 0.30 e n_trials < max → n_trials += 1 | |
| - punishment_rate < 0.05 e n_trials > min → n_trials -= 1 | |
| Args: | |
| trigger: "auto" (após train_hypotheses), "punishment" (após punição), | |
| "manual" (chamada externa). | |
| Returns: | |
| Dict com estado anterior, novo estado, e regras disparadas. | |
| """ | |
| before = { | |
| "n_hypotheses": int(self.n_hypotheses), | |
| "n_trials": int(self.n_trials), | |
| "hyp_train_steps": int(self.hyp_train_steps), | |
| } | |
| stats = self._compute_loss_stats() | |
| rate = self._compute_punishment_rate() | |
| rules_fired: List[str] = [] | |
| # 1. Ajuste de n_hypotheses (com base em volatilidade) | |
| if stats["n"] >= 3: | |
| if stats["volatility"] > 0.30 and self.n_hypotheses < self.max_n_hypotheses: | |
| new_n = min(self.n_hypotheses + 2, self.max_n_hypotheses) | |
| if new_n != self.n_hypotheses: | |
| self.n_hypotheses = new_n | |
| self.hypothesis_ensemble.set_active_count(new_n) | |
| rules_fired.append( | |
| f"n_hyp+2 (vol={stats['volatility']:.3f} → {new_n})" | |
| ) | |
| elif stats["volatility"] < 0.05 and self.n_hypotheses > self.min_n_hypotheses: | |
| new_n = max(self.n_hypotheses - 2, self.min_n_hypotheses) | |
| if new_n != self.n_hypotheses: | |
| self.n_hypotheses = new_n | |
| self.hypothesis_ensemble.set_active_count(new_n) | |
| rules_fired.append( | |
| f"n_hyp-2 (vol={stats['volatility']:.3f} → {new_n})" | |
| ) | |
| # 2. Ajuste de hyp_train_steps (com base em slope) | |
| if stats["n"] >= 3: | |
| if stats["slope"] > -0.001 and self.hyp_train_steps < self.max_hyp_train_steps: | |
| new_steps = min(self.hyp_train_steps + 5, self.max_hyp_train_steps) | |
| if new_steps != self.hyp_train_steps: | |
| self.hyp_train_steps = new_steps | |
| rules_fired.append( | |
| f"steps+5 (slope={stats['slope']:.5f} → {new_steps})" | |
| ) | |
| elif stats["slope"] < -0.01 and self.hyp_train_steps > self.min_hyp_train_steps: | |
| new_steps = max(self.hyp_train_steps - 5, self.min_hyp_train_steps) | |
| if new_steps != self.hyp_train_steps: | |
| self.hyp_train_steps = new_steps | |
| rules_fired.append( | |
| f"steps-5 (slope={stats['slope']:.5f} → {new_steps})" | |
| ) | |
| # 3. Ajuste de n_trials (com base em recorrência de punições) | |
| if len(self._punishment_history) >= 3: | |
| if rate > 0.30 and self.n_trials < self.max_n_trials: | |
| new_t = min(self.n_trials + 1, self.max_n_trials) | |
| if new_t != self.n_trials: | |
| self.n_trials = new_t | |
| rules_fired.append(f"n_trials+1 (rate={rate:.3f} → {new_t})") | |
| elif rate < 0.05 and self.n_trials > self.min_n_trials: | |
| new_t = max(self.n_trials - 1, self.min_n_trials) | |
| if new_t != self.n_trials: | |
| self.n_trials = new_t | |
| rules_fired.append(f"n_trials-1 (rate={rate:.3f} → {new_t})") | |
| after = { | |
| "n_hypotheses": int(self.n_hypotheses), | |
| "n_trials": int(self.n_trials), | |
| "hyp_train_steps": int(self.hyp_train_steps), | |
| } | |
| adapted = before != after | |
| record = { | |
| "trigger": trigger, | |
| "step": int(self._last_adaptation_step), | |
| "before": before, | |
| "after": after, | |
| "adapted": bool(adapted), | |
| "rules_fired": rules_fired, | |
| "loss_stats": stats, | |
| "punishment_rate": float(rate), | |
| } | |
| self._adaptation_log.append(record) | |
| # V6.5-V2-metrics-FIX: limita crescimento do log (era ilimitado, causando | |
| # crescimento de memória em treino longo). Mantém últimas 50 entradas. | |
| if len(self._adaptation_log) > 50: | |
| self._adaptation_log = self._adaptation_log[-50:] | |
| self._last_adaptation_step += 1 | |
| return record | |
| def get_adaptation_log(self) -> List[Dict[str, Any]]: | |
| """Retorna o log de adaptações dinâmicas (para monitoramento).""" | |
| return list(self._adaptation_log) | |
| # ================================================================== | |
| # V6.5-V2-metrics-FIX — Limpeza agressiva de memória RAM | |
| # ================================================================== | |
| # User requirement: "analisar e aprimorar (logicamente e matematicamente) | |
| # no projeto limpeza de memória RAM e de Armazenamento (ao concluir)". | |
| # | |
| # Mathematical analysis: | |
| # A memória RAM consumida pelo KLS V2 tem 4 fontes principais: | |
| # 1. Pesos do SOM: I*J*K*L*4 floats = 864*4 = 3456 floats (~14KB) | |
| # → FIXO (não pode ser limpo sem destruir o modelo) | |
| # 2. HypothesisEnsemble: max_n_hypotheses * DeltaGenerator params | |
| # = 32 * (864*256 + 256*256 + 256*3456) = 32 * 1.16M = ~37M params | |
| # → ~148MB em float32. Os OTIMIZADORAS Adam guardam 2x esse valor | |
| # (momentum + variance) = ~296MB adicionais. | |
| # 3. HypothesisClassifier: 864*512 + 512*256 + ... = ~616K params (~2.5MB) | |
| # 4. buffer_4d: até MAX_BUFFER_SIZE amostras * 4 floats = ~4KB (mínimo) | |
| # MAS os tensores carregam gradiente do embedding se não forem detached. | |
| # | |
| # A limpeza agressiva atua em: | |
| # - Zera gradientes acumulados nos otimizadores (Adam state) | |
| # - Chama gc.collect() para liberar ciclos Python | |
| # - Libera caches internos do PyTorch (oneDNN primitive cache) | |
| # - Trunca listas de histórico que crescem com o treino | |
| # ------------------------------------------------------------------ | |
| def aggressive_cleanup(self) -> Dict[str, Any]: | |
| """V6.5-V2-metrics-FIX — Limpeza agressiva de memória RAM do KLS. | |
| Retorna métricas antes/depois para confirmação. NÃO destroi pesos do | |
| SOM nem do classifier — apenas libera caches e gradientes acumulados. | |
| Returns: | |
| Dict com rss_before/after_mb, n_refs_collected, buffers_trimmed. | |
| """ | |
| import gc | |
| try: | |
| import resource | |
| rss_before = float(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) / 1024.0 | |
| except Exception: | |
| rss_before = 0.0 | |
| # 1. Zera gradientes do otimizador Adam (libera momentum/variance state) | |
| try: | |
| if hasattr(self, "hyp_optimizer"): | |
| self.hyp_optimizer.zero_grad(set_to_none=True) | |
| except Exception: | |
| pass | |
| # 2. Trunca históricos que crescem com o treino | |
| if len(self._adaptation_log) > 20: | |
| self._adaptation_log = self._adaptation_log[-20:] | |
| if len(self.inference_punishment_history) > 50: | |
| self.inference_punishment_history = self.inference_punishment_history[-50:] | |
| if len(self.vqvae2_metrics_history) > 30: | |
| self.vqvae2_metrics_history = self.vqvae2_metrics_history[-30:] | |
| if len(self.w8a8_metrics_history) > 30: | |
| self.w8a8_metrics_history = self.w8a8_metrics_history[-30:] | |
| # 3. gc.collect() em 2 passes (gerações 0+1 e 2) | |
| n_collected_0 = gc.collect(0) | |
| n_collected_1 = gc.collect(1) | |
| n_collected_2 = gc.collect(2) | |
| # 4. Libera caches CPU do PyTorch (oneDNN/MKL) | |
| try: | |
| import torch | |
| if hasattr(torch, "cuda") and torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| except Exception: | |
| pass | |
| try: | |
| import resource | |
| rss_after = float(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) / 1024.0 | |
| except Exception: | |
| rss_after = 0.0 | |
| return { | |
| "rss_before_mb": float(rss_before), | |
| "rss_after_mb": float(rss_after), | |
| "freed_mb": max(0.0, rss_before - rss_after), | |
| "gc_collected": int(n_collected_0 + n_collected_1 + n_collected_2), | |
| "adaptation_log_trimmed": int(len(self._adaptation_log)), | |
| "timestamp": __import__("datetime").datetime.utcnow().isoformat() + "Z", | |
| } | |
| # ================================================================== | |
| # 2.1 Treinamento dos geradores (train_hypotheses) | |
| # ================================================================== | |
| def train_hypotheses( | |
| self, | |
| buffer_4d: Optional[List[torch.Tensor]] = None, | |
| buffer_labels: Optional[List[int]] = None, | |
| ) -> Dict[str, Any]: | |
| """Treina os 16 geradores para produzir deltas que melhorem a classificação. | |
| Usa uma cópia do SOM para avaliar o efeito de cada delta e | |
| retropropaga o erro de classificação para os parâmetros dos geradores. | |
| Mathematical formulation: | |
| Para cada step s = 1..hyp_train_steps: | |
| 1. x_state = mean(som_activations(buffer)) # (1, P_som) | |
| 2. δ_h = scale * g_h(x_state) para h = 1..16 | |
| 3. loss_h = BCE(classifier(SOM+δ_h), labels) | |
| 4. loss = mean_h(loss_h) + λ * ||δ||² (regularização) | |
| 5. θ_{s+1} = θ_s - η * ∂loss/∂θ | |
| Returns: | |
| Dict com métricas do treino (loss inicial, final, n_steps). | |
| """ | |
| if buffer_4d is None: | |
| buffer_4d = self.buffer_4d | |
| if buffer_labels is None: | |
| buffer_labels = self.buffer_labels | |
| if not buffer_4d: | |
| return {"active": False, "reason": "empty_buffer"} | |
| import time as _time | |
| import gc as _gc | |
| t0 = _time.time() | |
| # V6.5-V2-oom-guard — Pré-check de memória antes de alocar tensores | |
| # grandes (n_hyp * P * N * 4 bytes). Se memória livre < 2x estimado, | |
| # trunca o buffer para evitar OOM-killer. | |
| # User requirement: "o processo vem sendo morto OOM-kiler". | |
| try: | |
| import os as _os | |
| # Estima uso: n_hyp * P * N * 4 bytes (float32) + overhead 50% | |
| _est_bytes = int(self.n_hypotheses * self.som_neuron_count * | |
| len(buffer_4d) * 4 * 1.5) | |
| _est_mb = _est_bytes / 1e6 | |
| # Se estimativa > 800MB, trunca buffer para últimos 64 samples | |
| if _est_mb > 800 and len(buffer_4d) > 64: | |
| _orig_n = len(buffer_4d) | |
| buffer_4d = buffer_4d[-64:] | |
| buffer_labels = buffer_labels[-64:] | |
| # Log silently (não podemos usar logger aqui para evitar circular import) | |
| print(f"[V6.5-V2-oom-guard] train_hypotheses: buffer truncated " | |
| f"{_orig_n}→64 (est. {_est_mb:.0f}MB > 800MB)") | |
| except Exception: | |
| pass # Nunca deixa o pré-check quebrar o treino | |
| # Prepara dados | |
| data = torch.stack(buffer_4d).detach() # (N, 4) | |
| labels = torch.tensor(buffer_labels, dtype=torch.float, device=data.device) | |
| # Computa ativações SOM para cada amostra (sem gradiente) | |
| with torch.no_grad(): | |
| som_activations = torch.stack( | |
| [self._som_activation(data[i]) for i in range(len(data))] | |
| ) # (N, P_som) | |
| # Snapshot dos pesos atuais do SOM (para simular ajustes) | |
| som_weights_flat = self.som.weights.detach().clone().flatten() # (P,) | |
| self.hypothesis_ensemble.train() | |
| losses = [] | |
| _oom_truncated = False # V6.5-V2-oom-guard: flag setada em caso de OOM | |
| _oom_step = -1 # V6.5-V2-oom-guard: step onde OOM ocorreu | |
| # V6.5-V2-metrics-FIX: pré-computa data_norm_sq uma única vez (N, 1) | |
| # para reuso em todos os steps — evita recomputação redundante. | |
| data_norm_sq = (data * data).sum(dim=-1, keepdim=True).t() # (1, N) | |
| labels_expanded = labels # (N,) | |
| for step in range(self.hyp_train_steps): | |
| # V6.5-V2-oom-guard — captura OOM/RuntimeError por step. | |
| # Em caso de OOM, interrompe o treino de hipóteses graciosamente. | |
| try: | |
| self.hyp_optimizer.zero_grad() | |
| # Ativação média como representação do estado do SOM | |
| x_mean = som_activations.mean(dim=0, keepdim=True) # (1, P_som) | |
| # Gera deltas: (1, n_hypotheses, P) | |
| deltas_stack = self.hypothesis_ensemble.forward_stacked(x_mean) | |
| deltas_stack = self.delta_scale * deltas_stack # escala | |
| # V6.5-V2-metrics-FIX: Avaliação VETORIZADA de todas as hipóteses | |
| # em paralelo (substitui loop que materializava 16 cópias do SOM). | |
| # Memória: (n_hyp, P, N) em vez de 16 * (P + 2*P*N). | |
| if self.classifier is not None and self.classifier_trained: | |
| # deltas_stack: (1, n_hyp, P) → (n_hyp, P, 4) reshape | |
| n_hyp = self.n_hypotheses | |
| P_neurons = self.som_neuron_count | |
| # som_weights_flat: (P, 4) detached | |
| W_base = som_weights_flat.reshape(P_neurons, 4) # (P, 4) | |
| # delta_h: (n_hyp, P) → reshape para (n_hyp, P, 4) | |
| deltas_3d = deltas_stack[0].reshape(n_hyp, P_neurons, 4) # (n_hyp, P, 4) | |
| # W_new[h, p, d] = W_base[p, d] + deltas_3d[h, p, d] | |
| W_new = W_base.unsqueeze(0) + deltas_3d # (n_hyp, P, 4) — broadcast | |
| # dist²[h, p, n] = ||W_new[h, p] - data[n]||² | |
| # = ||W_new[h, p]||² + ||data[n]||² - 2*W_new[h, p]·data[n] | |
| W_new_norm_sq = (W_new * W_new).sum(dim=-1) # (n_hyp, P) | |
| # cross[h, p, n] = W_new[h, p] · data[n] | |
| cross = torch.matmul(W_new, data.t()) # (n_hyp, P, N) | |
| dist_sq = ( | |
| W_new_norm_sq.unsqueeze(-1) # (n_hyp, P, 1) | |
| + data_norm_sq # (1, N) → broadcast (n_hyp, P, N) | |
| - 2.0 * cross | |
| ) # (n_hyp, P, N) | |
| dist_sq = torch.clamp(dist_sq, min=0.0) | |
| # activations[h, n, p] = dist_sq[h, p, n] | |
| activations = dist_sq.transpose(1, 2) # (n_hyp, N, P) | |
| # Classifier forward (congelado) | |
| classifier_params_were_grad = [ | |
| p.requires_grad for p in self.classifier.parameters() | |
| ] | |
| for p in self.classifier.parameters(): | |
| p.requires_grad_(False) | |
| try: | |
| logits = self.classifier( | |
| activations.reshape(n_hyp * len(data), P_neurons) | |
| ).reshape(n_hyp, len(data)) # (n_hyp, N) | |
| # BCE por hipótese, depois média | |
| loss_per_hyp = F.binary_cross_entropy_with_logits( | |
| logits, | |
| labels_expanded.unsqueeze(0).expand(n_hyp, -1), | |
| reduction='none', | |
| ).mean(dim=1) # (n_hyp,) | |
| loss_total = loss_per_hyp.mean() | |
| # Regularização L2 sobre os deltas | |
| reg_loss = 0.01 * deltas_stack.norm() | |
| loss_total = loss_total + reg_loss | |
| finally: | |
| for p, was_grad in zip(self.classifier.parameters(), | |
| classifier_params_were_grad): | |
| p.requires_grad_(was_grad) | |
| else: | |
| # Fallback: minimizar norma do delta (regularização pura) | |
| loss_total = deltas_stack.norm() | |
| loss_total.backward() | |
| # V6.5-V2-metrics-FIX-3 — gradient clipping no treino de hipóteses | |
| # previne explosão de gradientes em batches degenerados (ex: todos | |
| # os labels iguais → BCE produz gradientes grandes). max_norm=1.0 | |
| # é o valor canônico recomendado pela literatura para BCE heads. | |
| torch.nn.utils.clip_grad_norm_( | |
| self.hypothesis_ensemble.parameters(), max_norm=1.0 | |
| ) | |
| # V6.5-V2-metrics-FIX-3 — zera grad do delta_scale (que não está | |
| # no optimizer mas aparece no grafo de forward, aculumando .grad | |
| # silenciosamente a cada step). | |
| if self.delta_scale.grad is not None: | |
| self.delta_scale.grad = None | |
| self.hyp_optimizer.step() | |
| losses.append(float(loss_total.item())) | |
| # V6.5-V2-metrics-FIX: libera tensores intermediários explicitamente | |
| # para reduzir pico de memória entre steps. | |
| del loss_total, deltas_stack | |
| if 'W_new' in dir(): | |
| del W_new, cross, dist_sq, activations | |
| # Atualiza a escala de delta (decai suavemente) | |
| with torch.no_grad(): | |
| self.delta_scale.data = torch.clamp( | |
| self.delta_scale.data * 0.99, 0.001, 0.1 | |
| ) | |
| except (RuntimeError, MemoryError) as _oom_err: | |
| # V6.5-V2-oom-guard — OOM detectado durante o step. | |
| # Interrompe o treino de hipóteses e retorna partial results. | |
| _oom_truncated = True | |
| _oom_step = step | |
| try: | |
| _gc.collect() | |
| except Exception: | |
| pass | |
| break | |
| self.hypothesis_ensemble.eval() | |
| # V6.5-V2-metrics-FIX: libera som_activations e som_weights_flat | |
| del som_activations, som_weights_flat, data, data_norm_sq | |
| import gc | |
| gc.collect() | |
| # V6.5-V2-dynamic — registra loss_final no histórico | |
| loss_final_val = float(losses[-1]) if losses else 0.0 | |
| self._record_loss(loss_final_val) | |
| # V6.5-V2-metrics-FIX-2 — atualiza contadores cumulativos | |
| # User requirement: "quantos passos de treino de hipóteses usado" | |
| self._total_hyp_steps_executed += int(self.hyp_train_steps) | |
| self._n_train_hyp_calls += 1 | |
| result = { | |
| "active": True, | |
| "n_steps": self.hyp_train_steps, | |
| "n_steps_executed": len(losses), | |
| "oom_truncated": bool(_oom_truncated), | |
| "oom_step": int(_oom_step), | |
| "n_hypotheses": self.n_hypotheses, | |
| "loss_initial": float(losses[0]) if losses else 0.0, | |
| "loss_final": loss_final_val, | |
| "loss_mean": float(sum(losses) / len(losses)) if losses else 0.0, | |
| "delta_scale_final": float(self.delta_scale.item()), | |
| "elapsed_ms": float((_time.time() - t0) * 1000), | |
| # V6.5-V2-metrics-FIX-2 — contadores cumulativos para monitoramento | |
| "total_hyp_steps_executed": int(self._total_hyp_steps_executed), | |
| "n_train_hyp_calls": int(self._n_train_hyp_calls), | |
| } | |
| # V6.5-V2-dynamic — dispara adaptação automática de hiperparâmetros | |
| try: | |
| adapt_record = self._adapt_hyperparameters(trigger="auto") | |
| result["adaptation"] = adapt_record | |
| except Exception as adapt_err: | |
| result["adaptation_error"] = str(adapt_err)[:200] | |
| return result | |
| def _compute_classification_loss_with_som( | |
| self, som_weights: torch.Tensor, data: torch.Tensor, labels: torch.Tensor | |
| ) -> torch.Tensor: | |
| """V6.5-V2-metrics-FIX — Computa perda de classificação com SOM temporário. | |
| BUG CORRIGIDO (V6.5-V2-metrics-FIX): | |
| A versão anterior envolvia toda a computação em `torch.no_grad()`, | |
| o que matava o gradiente que deveria fluir dos deltas → SOM → loss. | |
| Resultado: train_hypotheses() não treinava os geradores de fato. | |
| CORREÇÃO: A computação das ativações SOM (squared distances) é | |
| feita SEM no_grad, permitindo que o gradiente flua dos deltas | |
| (que pertencem ao HypothesisEnsemble) através do SOM temporário | |
| até a loss. Apenas o classifier é congelado (requires_grad=False) | |
| para que seu gradiente não seja atualizado por train_hypotheses. | |
| Mathematical formulation (squared Euclidean distance, consistente com | |
| _som_activation que usa ||W - x||^2, não ||W - x||): | |
| Para cada neurônio p e amostra n: | |
| dist²[p, n] = ||W[p] - x[n]||² | |
| = ||W[p]||² + ||x[n]||² - 2·W[p]·x[n] | |
| activations = dist².t() → (N, P_neurons) | |
| logits = classifier(activations) → (N,) | |
| loss = BCEWithLogitsLoss(logits, labels) | |
| Args: | |
| som_weights: tensor (I, J, K, L, 4) com gradiente (dos deltas). | |
| data: tensor (N, 4) — amostras 4D (detached). | |
| labels: tensor (N,) — rótulos binários. | |
| Returns: | |
| Scalar loss tensor (com gradiente fluindo para som_weights). | |
| """ | |
| if self.classifier is not None and self.classifier_trained: | |
| # NÃO usa torch.no_grad() — gradiente deve fluir dos deltas | |
| # através do SOM temporário até a loss. | |
| W_flat = som_weights.reshape(-1, 4) # (P_neurons, 4) | |
| # Identidade algébrica: ||W - x||^2 = ||W||^2 + ||x||^2 - 2*W·x | |
| # Evita materializar tensor (P, N, 4) — usa (P, N) direto. | |
| W_norm_sq = (W_flat * W_flat).sum(dim=-1, keepdim=True) # (P, 1) | |
| data_norm_sq = (data * data).sum(dim=-1, keepdim=True).t() # (1, N) | |
| cross = W_flat @ data.t() # (P, N) — matmul eficiente | |
| dist_sq = W_norm_sq + data_norm_sq - 2.0 * cross # (P, N) | |
| dist_sq = torch.clamp(dist_sq, min=0.0) # estabilidade numérica | |
| activations = dist_sq.t() # (N, P_neurons) — gradiente fluindo! | |
| # Congela classifier durante train_hypotheses (não queremos | |
| # atualizar seus pesos — apenas os deltas dos geradores) | |
| classifier_params_were_grad = [ | |
| p.requires_grad for p in self.classifier.parameters() | |
| ] | |
| for p in self.classifier.parameters(): | |
| p.requires_grad_(False) | |
| try: | |
| logits = self.classifier(activations) # (N,) | |
| loss = F.binary_cross_entropy_with_logits(logits, labels) | |
| finally: | |
| # Restaura requires_grad do classifier | |
| for p, was_grad in zip(self.classifier.parameters(), | |
| classifier_params_were_grad): | |
| p.requires_grad_(was_grad) | |
| return loss | |
| else: | |
| # Fallback: votação BMU (não-diferenciável) | |
| with torch.no_grad(): | |
| if not hasattr(self, "neuron_label"): | |
| self._label_neurons() | |
| correct = 0 | |
| for i in range(len(data)): | |
| bmu = self.som.find_bmu(data[i]) | |
| pred = self.neuron_label.get(bmu, 0.5) | |
| if (pred > 0.5) == (labels[i].item() > 0.5): | |
| correct += 1 | |
| return torch.tensor( | |
| 1.0 - correct / max(1, len(data)), | |
| device=data.device, | |
| dtype=torch.float, | |
| ) | |
| # ================================================================== | |
| # 2.2 Seleção do melhor delta (3 tentativas × 16 hipóteses) | |
| # ================================================================== | |
| def select_best_delta( | |
| self, | |
| buffer_4d: Optional[List[torch.Tensor]] = None, | |
| buffer_labels: Optional[List[int]] = None, | |
| n_trials: Optional[int] = None, | |
| ) -> Tuple[Optional[torch.Tensor], float, Dict[str, Any]]: | |
| """Avalia 3 tentativas × 16 hipóteses e retorna o melhor delta e sua acurácia. | |
| Args: | |
| buffer_4d, buffer_labels: opcional — usa self.buffer_* se None. | |
| n_trials: opcional — usa self.n_trials se None. | |
| Returns: | |
| Tuple (best_delta, best_acc, info_dict): | |
| - best_delta: tensor (P,) ou None se buffer vazio | |
| - best_acc: acurácia obtida com o melhor delta | |
| - info_dict: métricas da seleção (n_trials, n_hypotheses, etc.) | |
| """ | |
| if buffer_4d is None: | |
| buffer_4d = self.buffer_4d | |
| if buffer_labels is None: | |
| buffer_labels = self.buffer_labels | |
| if not buffer_4d: | |
| return None, -1.0, {"active": False, "reason": "empty_buffer"} | |
| if n_trials is None: | |
| n_trials = self.n_trials | |
| import time as _time | |
| t0 = _time.time() | |
| data = torch.stack(buffer_4d).detach() | |
| labels = torch.tensor(buffer_labels, dtype=torch.float, device=data.device) | |
| # Computa ativação SOM média | |
| with torch.no_grad(): | |
| som_activations = torch.stack( | |
| [self._som_activation(data[i]) for i in range(len(data))] | |
| ) | |
| x_state = som_activations.mean(dim=0, keepdim=True) | |
| best_delta = None | |
| best_acc = -1.0 | |
| trial_results = [] | |
| som_flat = self.som.weights.detach().clone().flatten() | |
| self.hypothesis_ensemble.eval() | |
| for trial in range(n_trials): | |
| # Adiciona ruído à entrada para diversificar tentativas (trial > 0) | |
| if trial > 0: | |
| x_noisy = x_state + 0.01 * torch.randn_like(x_state) | |
| else: | |
| x_noisy = x_state | |
| with torch.no_grad(): | |
| deltas_stack = self.hypothesis_ensemble.forward_stacked(x_noisy) | |
| deltas_stack = self.delta_scale * deltas_stack | |
| # Avalia cada uma das 16 hipóteses | |
| trial_accs = [] | |
| for h in range(self.n_hypotheses): | |
| delta_h = deltas_stack[0, h] | |
| som_temp = som_flat + delta_h | |
| som_temp_reshaped = som_temp.view_as(self.som.weights) | |
| acc = self._evaluate_accuracy_with_som( | |
| som_temp_reshaped, data, labels | |
| ) | |
| trial_accs.append(acc) | |
| if acc > best_acc: | |
| best_acc = acc | |
| best_delta = delta_h.clone() | |
| trial_results.append({ | |
| "trial": trial + 1, | |
| "best_acc_in_trial": float(max(trial_accs)) if trial_accs else 0.0, | |
| "mean_acc_in_trial": float(sum(trial_accs) / len(trial_accs)) if trial_accs else 0.0, | |
| "best_hyp_idx": int(trial_accs.index(max(trial_accs))) if trial_accs else -1, | |
| }) | |
| info = { | |
| "active": True, | |
| "n_trials": n_trials, | |
| "n_hypotheses": self.n_hypotheses, | |
| "best_acc": float(best_acc), | |
| "delta_norm": float(best_delta.norm().item()) if best_delta is not None else 0.0, | |
| "delta_scale": float(self.delta_scale.item()), | |
| "trials": trial_results, | |
| "elapsed_ms": float((_time.time() - t0) * 1000), | |
| } | |
| return best_delta, best_acc, info | |
| def _evaluate_accuracy_with_som( | |
| self, som_weights: torch.Tensor, data: torch.Tensor, labels: torch.Tensor | |
| ) -> float: | |
| """V6.5-V2-metrics-FIX — Calcula acurácia com SOM temporário (vetorizado). | |
| Substitui o loop que chamava _som_activation(data[i]) para cada amostra | |
| por uma única operação matmul. Reduz tempo de O(N*P) para O(P*N) mas | |
| com paralelismo SIMD (single matmul vs N python iterations). | |
| Para o fallback (votação BMU), ainda precisa trocar self.som.weights | |
| temporariamente pois find_bmu lê self.som.weights diretamente. | |
| """ | |
| if self.classifier is not None and self.classifier_trained: | |
| with torch.no_grad(): | |
| # Vetorizado: dist²[p, n] = ||W[p]||² + ||x[n]||² - 2*W[p]·x[n] | |
| W_flat = som_weights.reshape(-1, 4) # (P, 4) | |
| W_norm_sq = (W_flat * W_flat).sum(dim=-1, keepdim=True) # (P, 1) | |
| data_norm_sq = (data * data).sum(dim=-1, keepdim=True).t() # (1, N) | |
| cross = W_flat @ data.t() # (P, N) | |
| dist_sq = W_norm_sq + data_norm_sq - 2.0 * cross # (P, N) | |
| dist_sq = torch.clamp(dist_sq, min=0.0) | |
| activations = dist_sq.t() # (N, P) | |
| logits = self.classifier(activations) # (N,) | |
| preds = (logits > 0.5).float() | |
| acc = (preds == labels).float().mean().item() | |
| return float(acc) | |
| else: | |
| # Fallback: votação BMU — precisa trocar self.som.weights | |
| original_weights = self.som.weights | |
| self.som.weights = som_weights | |
| try: | |
| with torch.no_grad(): | |
| if not hasattr(self, "neuron_label"): | |
| self._label_neurons() | |
| correct = 0 | |
| for i in range(len(data)): | |
| bmu = self.som.find_bmu(data[i]) | |
| pred = self.neuron_label.get(bmu, 0.5) | |
| if (pred > 0.5) == (labels[i].item() > 0.5): | |
| correct += 1 | |
| acc = correct / max(1, len(data)) | |
| return float(acc) | |
| finally: | |
| self.som.weights = original_weights | |
| # ================================================================== | |
| # 2.3 Aplicação do melhor delta + consolidação via EWC | |
| # ================================================================== | |
| def apply_best_delta_and_consolidate( | |
| self, | |
| buffer_4d: Optional[List[torch.Tensor]] = None, | |
| buffer_labels: Optional[List[int]] = None, | |
| ) -> Dict[str, Any]: | |
| """Seleciona o melhor delta, aplica ao SOM e executa EWC. | |
| Pipeline: | |
| 1. select_best_delta: avalia 3 tentativas × 16 hipóteses | |
| 2. Aplica δ* ao SOM: SOM ← SOM + δ* | |
| 3. Re-avalia acurácia após aplicação | |
| 4. Consolida via EWC: set_ewc_reference() (Fisher em w) | |
| Returns: | |
| Dict com métricas (acc_before, acc_after, delta_norm, etc.) | |
| """ | |
| import time as _time | |
| t0 = _time.time() | |
| # Avalia acurácia ANTES da aplicação | |
| acc_before = self.evaluate_classification() | |
| # Seleciona melhor delta | |
| best_delta, best_acc, sel_info = self.select_best_delta( | |
| buffer_4d, buffer_labels | |
| ) | |
| if best_delta is None: | |
| return { | |
| "active": False, | |
| "reason": "no_valid_delta", | |
| "elapsed_ms": float((_time.time() - t0) * 1000), | |
| } | |
| # V6.5-V2-metrics-FIX-2 — Verifica se best_delta contém NaN/Inf ANTES | |
| # de aplicar ao SOM. User requirement: "investigar QE e KL resultando | |
| # em NAN". Se o delta tem NaN (devido a gradientes explosivos em | |
| # train_hypotheses), aplicar ao SOM contaminaria os pesos e propagaria | |
| # NaN para todas as métricas subsequentes (QE, KL, etc.). | |
| if torch.isnan(best_delta).any() or torch.isinf(best_delta).any(): | |
| n_nan = int(torch.isnan(best_delta).sum().item()) | |
| n_inf = int(torch.isinf(best_delta).sum().item()) | |
| logger = logging.getLogger(__name__) if 'logging' in dir() else None | |
| import logging as _logging | |
| _logging.getLogger(__name__).warning( | |
| f"[V6.5-V2-metrics-FIX-2] best_delta contains NaN/Inf " | |
| f"(nan={n_nan}, inf={n_inf}). SKIPPING application to SOM " | |
| f"to prevent weight contamination. Investigate train_hypotheses " | |
| f"for gradient explosion (consider lowering hyp_lr or adding " | |
| f"gradient clipping)." | |
| ) | |
| return { | |
| "active": False, | |
| "reason": f"best_delta_has_nan (nan={n_nan}, inf={n_inf})", | |
| "delta_norm": float(best_delta.norm().item()), | |
| "elapsed_ms": float((_time.time() - t0) * 1000), | |
| "nan_detected": True, | |
| } | |
| # Aplica o delta ao SOM | |
| with torch.no_grad(): | |
| som_flat = self.som.weights.flatten() | |
| som_flat_new = som_flat + best_delta | |
| # V6.5-V2-metrics-FIX-2 — verificação pós-aplicação: se os pesos | |
| # do SOM ficaram com NaN (deveria não acontecer após o check acima, | |
| # mas defense in depth), reverte para o estado anterior. | |
| if torch.isnan(som_flat_new).any() or torch.isinf(som_flat_new).any(): | |
| import logging as _logging | |
| _logging.getLogger(__name__).error( | |
| f"[V6.5-V2-metrics-FIX-2] SOM weights became NaN/Inf after " | |
| f"delta application. REVERTING to previous weights." | |
| ) | |
| else: | |
| self.som.weights = som_flat_new.view_as(self.som.weights) | |
| # Re-avalia acurácia APÓS aplicação | |
| acc_after = self.evaluate_classification() | |
| # Re-rotula neurônios (BMU labels podem ter mudado) | |
| self._label_neurons() | |
| # V6.5-V2-metrics-FIX-3 — se best_delta tinha NaN (detectado acima e | |
| # retornado early), NÃO chegamos aqui. Mas se chegamos via caminho | |
| # else (pesos ficaram NaN após aplicação e foram revertidos), NÃO | |
| # devemos consolidar via EWC — consolidar pesos potencialmente | |
| # corruptos como referência EWC propagaria o problema para o futuro. | |
| # Verificação: se os pesos atuais têm NaN, pula set_ewc_reference. | |
| weights_corrupt = torch.isnan(self.som.weights).any() or torch.isinf(self.som.weights).any() | |
| if weights_corrupt: | |
| import logging as _logging | |
| _logging.getLogger(__name__).error( | |
| f"[V6.5-V2-metrics-FIX-3] SOM weights contain NaN/Inf after " | |
| f"delta application — SKIPPING set_ewc_reference to avoid " | |
| f"propagating corruption. Weights will be sanitized on next " | |
| f"update_weights call." | |
| ) | |
| # Sanitiza imediatamente para permitir continuidade do treino | |
| self.som.weights = torch.nan_to_num( | |
| self.som.weights, nan=0.0, posinf=100.0, neginf=-100.0 | |
| ) | |
| ewc_set = False | |
| sel_info["ewc_skipped"] = "weights_corrupt_after_delta" | |
| else: | |
| # Consolida via EWC (Fisher information em w) | |
| try: | |
| self.som.set_ewc_reference() | |
| ewc_set = True | |
| except Exception as e: | |
| ewc_set = False | |
| sel_info["ewc_error"] = str(e)[:200] | |
| return { | |
| "active": True, | |
| "acc_before": float(acc_before), | |
| "acc_after": float(acc_after), | |
| "best_acc_during_selection": float(best_acc), | |
| "delta_norm": float(best_delta.norm().item()), | |
| "delta_scale": float(self.delta_scale.item()), | |
| "ewc_reference_set": bool(ewc_set), | |
| "selection_info": sel_info, | |
| "elapsed_ms": float((_time.time() - t0) * 1000), | |
| } | |
| # ================================================================== | |
| # 2.4 Sobrescrita do process_batch para o novo protocolo V2 | |
| # ================================================================== | |
| def process_batch_v2( | |
| self, | |
| sentences: List[str], | |
| labels: List[int], | |
| label_strings: Optional[Dict[int, str]] = None, | |
| dataset_name: Optional[str] = None, | |
| enable_punishment: bool = True, | |
| ) -> Dict[str, Any]: | |
| """Versão V2 do process_batch com 3 tentativas e 16 hipóteses. | |
| Protocolo: | |
| - CONHECIMENTO (enable_punishment=False): apenas adiciona dados e | |
| treina SOM (sem aplicar punição). | |
| - TREINAMENTO COM PUNIÇÃO (enable_punishment=True): | |
| * acc < 1.0 → punishment_count += 1 | |
| * punishment_count == 1: treinar 16 hipóteses + activate_hypothesis | |
| * punishment_count == 2: aplicar melhor delta (3×16) + EWC + reset | |
| Args: | |
| sentences, labels, label_strings, dataset_name: mesmos de process_batch. | |
| enable_punishment: se False, apenas acumula conhecimento (fase 1). | |
| Returns: | |
| Dict com métricas do batch processado. | |
| """ | |
| import time as _time | |
| t0 = _time.time() | |
| result: Dict[str, Any] = { | |
| "phase": "CONHECIMENTO" if not enable_punishment else "PUNICAO", | |
| "dataset": dataset_name, | |
| "n_samples": len(sentences), | |
| "action": "none", | |
| "cycle_completed": False, | |
| } | |
| # 1. Adiciona dados ao buffer | |
| self.add_data( | |
| sentences, labels, | |
| label_strings=label_strings, | |
| dataset_name=dataset_name, | |
| ) | |
| # 2. Verifica início de treino | |
| training_started = self.check_training_start() | |
| if training_started or self.training_ready: | |
| # 3. Treina SOM no buffer (regra de Kohonen) | |
| self.train_som_on_buffer() | |
| self._label_neurons() | |
| result["som_trained"] = True | |
| if not self.training_ready: | |
| result["elapsed_ms"] = float((_time.time() - t0) * 1000) | |
| return result | |
| # 4. Fase de CONHECIMENTO: para aqui, sem aplicar punição | |
| if not enable_punishment: | |
| acc = self.evaluate_classification() | |
| result["accuracy"] = float(acc) | |
| result["elapsed_ms"] = float((_time.time() - t0) * 1000) | |
| return result | |
| # 5. Fase de PUNIÇÃO: avalia e aplica protocolo V2 | |
| acc = self.evaluate_classification() | |
| result["accuracy"] = float(acc) | |
| if acc < 1.0: | |
| self.punishment_count += 1 | |
| self.success_count = 0 | |
| # V6.5-V2-dynamic — registra evento de punição | |
| self._record_punishment_event(True) | |
| if self.punishment_count == 1: | |
| # 1ª punição: treinar hipóteses + ativar classificador | |
| hyp_info = self.train_hypotheses() | |
| if self.classifier is None: | |
| self.activate_hypothesis() | |
| else: | |
| # Re-treina classifier com buffer atual | |
| self.activate_hypothesis() | |
| result["action"] = "train_hypotheses_and_activate" | |
| result["hypotheses_training"] = hyp_info | |
| elif self.punishment_count >= 2: | |
| # 2ª punição: selecionar melhor delta (n_trials × n_hyp) + aplicar + EWC | |
| apply_info = self.apply_best_delta_and_consolidate() | |
| result["action"] = "apply_best_delta_and_consolidate" | |
| result["delta_application"] = apply_info | |
| # V6.5-V2-dynamic — adapta hiperparâmetros após punição recorrente | |
| try: | |
| adapt_record = self._adapt_hyperparameters(trigger="punishment") | |
| result["adaptation"] = adapt_record | |
| except Exception as adapt_err: | |
| result["adaptation_error"] = str(adapt_err)[:200] | |
| # Reset do ciclo (mirror training) | |
| self.training_ready = False | |
| self.punishment_count = 0 | |
| self.success_count = 0 | |
| self.histogram.clear() | |
| # Mantém um sliding window do buffer para preservar contexto | |
| if len(self.buffer_4d) > 256: | |
| self.buffer_4d = self.buffer_4d[-256:] | |
| self.buffer_labels = self.buffer_labels[-256:] | |
| result["cycle_completed"] = True | |
| else: | |
| # acc == 1.0: sucesso | |
| self.punishment_count = 0 | |
| self.success_count += 1 | |
| # V6.5-V2-dynamic — registra sucesso (sem punição) | |
| self._record_punishment_event(False) | |
| result["action"] = "success" | |
| result["elapsed_ms"] = float((_time.time() - t0) * 1000) | |
| return result | |
| # ================================================================== | |
| # 2.5 API de monitoramento V2 | |
| # ================================================================== | |
| def get_v2_metrics(self) -> Dict[str, Any]: | |
| """Retorna métricas específicas da versão V2 (inclui adaptação dinâmica).""" | |
| return { | |
| "version": "V2-dynamic", | |
| "n_hypotheses": int(self.n_hypotheses), | |
| "n_hypotheses_active": int(self.hypothesis_ensemble.active_count), | |
| "max_n_hypotheses": int(self.max_n_hypotheses), | |
| "n_trials": int(self.n_trials), | |
| "hyp_train_steps": int(self.hyp_train_steps), | |
| "hyp_lr": float(self.hyp_lr), | |
| "delta_scale": float(self.delta_scale.item()), | |
| "n_generators": int(len(self.hypothesis_ensemble.generators)), | |
| "punishment_count": int(self.punishment_count), | |
| "success_count": int(self.success_count), | |
| "training_ready": bool(self.training_ready), | |
| "classifier_trained": bool(self.classifier_trained), | |
| "ewc_reference_set": bool(self.som.old_weights_w is not None), | |
| "buffer_size": int(len(self.buffer_4d)), | |
| # V6.5-V2-metrics-FIX-2 — contadores cumulativos (user requirement: | |
| # "quantos passos de treino de hipóteses usado") | |
| "total_hyp_steps_executed": int(getattr(self, "_total_hyp_steps_executed", 0)), | |
| "n_train_hyp_calls": int(getattr(self, "_n_train_hyp_calls", 0)), | |
| "dynamic_adaptation": { | |
| "loss_history_len": int(len(self._loss_history)), | |
| "loss_stats": self._compute_loss_stats(), | |
| "punishment_rate": float(self._compute_punishment_rate()), | |
| "punishment_window_size": int(len(self._punishment_history)), | |
| "n_adaptations": int(len(self._adaptation_log)), | |
| "limits": { | |
| "min_n_hypotheses": int(self.min_n_hypotheses), | |
| "max_n_hypotheses": int(self.max_n_hypotheses), | |
| "min_n_trials": int(self.min_n_trials), | |
| "max_n_trials": int(self.max_n_trials), | |
| "min_hyp_train_steps": int(self.min_hyp_train_steps), | |
| "max_hyp_train_steps": int(self.max_hyp_train_steps), | |
| }, | |
| "last_5_adaptations": self._adaptation_log[-5:] if self._adaptation_log else [], | |
| }, | |
| } | |
| # ================================================================== | |
| # V6.5-V2-metrics-FIX-2 — Detecção de atividade de hipóteses e neurônios | |
| # ================================================================== | |
| # User requirement (latest): "Aprimorar a FASE2 PUNITIVA investigar QE e KL | |
| # resultando em NAN, acrescentar detecção da atividade das layers (quantas | |
| # ativadas) de hipóteses e das ativações dos neurônios, quantos passos de | |
| # treino de hipóteses usado". | |
| # | |
| # Mathematical formulation: | |
| # | |
| # Hipótese ativa: Um gerador g_h (h ∈ {1..n_hypotheses}) é considerado | |
| # "ativo" em um batch se o delta que ele produz tem norma L2 > threshold | |
| # (default 1e-6). Deltas com norma ≈ 0 indicam que o gerador está | |
| # produzindo saída trivial (morto) — não contribui para a diversidade | |
| # do ensemble. | |
| # | |
| # active_h = 1 if ||δ_h||₂ > threshold else 0 | |
| # n_active_hypotheses = Σ_h active_h | |
| # | |
| # Neurônio ativo: Um neurônio (i,j,k,l) é "ativo" se é BMU para pelo | |
| # menos uma amostra do buffer atual. Reuso da lógica de dead_neuron_rate, | |
| # mas exposto como métrica explícita para monitoramento contínuo. | |
| # | |
| # n_active_neurons = |{W_bmu(x_i) : i ∈ buffer}| | |
| # n_dead_neurons = n_total - n_active_neurons | |
| # neuron_activation_rate = n_active_neurons / n_total | |
| # | |
| # Passos de treino de hipóteses: Número total de passos de Adam executados | |
| # em train_hypotheses() desde o início do treino. Cumulativo ao longo de | |
| # todas as chamadas (punições). | |
| # | |
| # total_hyp_train_steps = Σ_call self.hyp_train_steps (na época da call) | |
| # ------------------------------------------------------------------ | |
| def detect_hypothesis_activity( | |
| self, | |
| threshold: float = 1e-6, | |
| ) -> Dict[str, Any]: | |
| """V6.5-V2-metrics-FIX-2 — Detecta quantas hipóteses estão ativas. | |
| User requirement: "acrescentar detecção da atividade das layers | |
| (quantas ativadas) de hipóteses". | |
| Executa um forward pass do HypothesisEnsemble sobre a ativação média | |
| do SOM no buffer atual e conta quantos dos active_count geradores | |
| produzem deltas com norma > threshold. | |
| Args: | |
| threshold: norma L2 mínima para considerar um delta "ativo". | |
| Returns: | |
| Dict com: | |
| - n_hypotheses_active: int (quantas hipóteses produziram delta ≠ 0) | |
| - n_hypotheses_total: int (active_count atual do ensemble) | |
| - max_n_hypotheses: int (limite máximo pré-alocado) | |
| - delta_norms: list[float] (norma de cada delta, para inspeção) | |
| - active_mask: list[bool] (True para hipóteses ativas) | |
| - activation_rate: float (n_active / n_total) | |
| - mean_delta_norm: float (média das normas) | |
| - max_delta_norm: float (maior norma) | |
| - threshold_used: float | |
| """ | |
| if not self.buffer_4d: | |
| return { | |
| "n_hypotheses_active": 0, | |
| "n_hypotheses_total": int(self.hypothesis_ensemble.active_count), | |
| "max_n_hypotheses": int(self.max_n_hypotheses), | |
| "delta_norms": [], | |
| "active_mask": [], | |
| "activation_rate": 0.0, | |
| "mean_delta_norm": 0.0, | |
| "max_delta_norm": 0.0, | |
| "threshold_used": float(threshold), | |
| "reason": "empty_buffer", | |
| } | |
| try: | |
| import torch as _torch | |
| with _torch.no_grad(): | |
| # Computa ativação SOM média do buffer | |
| data = _torch.stack(self.buffer_4d).detach() | |
| som_activations = _torch.stack( | |
| [self._som_activation(data[i]) for i in range(len(data))] | |
| ) | |
| x_mean = som_activations.mean(dim=0, keepdim=True) # (1, P_som) | |
| # Forward do ensemble: retorna (1, n_hyp, P) | |
| deltas_stack = self.hypothesis_ensemble.forward_stacked(x_mean) | |
| deltas_stack = self.delta_scale * deltas_stack # aplica escala | |
| # Norma L2 de cada delta: (n_hyp,) | |
| delta_norms = deltas_stack[0].norm(dim=-1) # (n_hyp,) | |
| delta_norms_list = [float(x) for x in delta_norms] | |
| active_mask = [bool(x > threshold) for x in delta_norms_list] | |
| n_active = sum(active_mask) | |
| # Libera tensores | |
| del deltas_stack, som_activations, data, x_mean, delta_norms | |
| return { | |
| "n_hypotheses_active": int(n_active), | |
| "n_hypotheses_total": int(self.hypothesis_ensemble.active_count), | |
| "max_n_hypotheses": int(self.max_n_hypotheses), | |
| "delta_norms": delta_norms_list, | |
| "active_mask": active_mask, | |
| "activation_rate": float(n_active / max(1, self.hypothesis_ensemble.active_count)), | |
| "mean_delta_norm": float(sum(delta_norms_list) / max(1, len(delta_norms_list))), | |
| "max_delta_norm": float(max(delta_norms_list) if delta_norms_list else 0.0), | |
| "threshold_used": float(threshold), | |
| } | |
| except Exception as e: | |
| return { | |
| "n_hypotheses_active": 0, | |
| "n_hypotheses_total": int(self.hypothesis_ensemble.active_count), | |
| "max_n_hypotheses": int(self.max_n_hypotheses), | |
| "delta_norms": [], | |
| "active_mask": [], | |
| "activation_rate": 0.0, | |
| "mean_delta_norm": 0.0, | |
| "max_delta_norm": 0.0, | |
| "threshold_used": float(threshold), | |
| "error": str(e)[:200], | |
| } | |
| def count_active_neurons(self) -> Dict[str, Any]: | |
| """V6.5-V2-metrics-FIX-2 — Conta neurônios ativos (BMUs) no buffer. | |
| User requirement: "acrescentar (...) das ativações dos neurônios". | |
| Para cada amostra no buffer_4d, encontra o BMU. Conta quantos | |
| neurônios únicos são BMUs (n_active_neurons) e quantos nunca são | |
| BMUs (n_dead_neurons = n_total - n_active). | |
| Returns: | |
| Dict com: | |
| - n_active_neurons: int (neurônios que são BMU para ≥1 amostra) | |
| - n_dead_neurons: int (neurônios que nunca são BMU) | |
| - n_total_neurons: int (total de neurônios no SOM) | |
| - neuron_activation_rate: float (n_active / n_total) | |
| - bmu_distribution: dict {flat_idx: count} (top-20 mais frequentes) | |
| """ | |
| if not self.buffer_4d: | |
| return { | |
| "n_active_neurons": 0, | |
| "n_dead_neurons": int(self.som_neuron_count), | |
| "n_total_neurons": int(self.som_neuron_count), | |
| "neuron_activation_rate": 0.0, | |
| "bmu_distribution": {}, | |
| "reason": "empty_buffer", | |
| } | |
| try: | |
| import torch as _torch | |
| with _torch.no_grad(): | |
| data = _torch.stack(self.buffer_4d).detach() | |
| flat_w = self.som.weights.view(-1, 4).detach() | |
| # (N, M) distâncias | |
| diff = data.unsqueeze(1) - flat_w.unsqueeze(0) | |
| dist_sq = _torch.sum(diff * diff, dim=-1) | |
| _, bmu_flat = _torch.min(dist_sq, dim=1) # (N,) | |
| bmu_list = bmu_flat.tolist() | |
| unique_bmus = set(bmu_list) | |
| n_active = len(unique_bmus) | |
| n_total = self.som_neuron_count | |
| # Distribuição dos BMUs (top-20) | |
| from collections import Counter | |
| bmu_counts = Counter(bmu_list) | |
| bmu_distribution = dict(bmu_counts.most_common(20)) | |
| del data, flat_w, diff, dist_sq, bmu_flat | |
| return { | |
| "n_active_neurons": int(n_active), | |
| "n_dead_neurons": int(n_total - n_active), | |
| "n_total_neurons": int(n_total), | |
| "neuron_activation_rate": float(n_active / max(1, n_total)), | |
| "bmu_distribution": {str(k): int(v) for k, v in bmu_distribution.items()}, | |
| } | |
| except Exception as e: | |
| return { | |
| "n_active_neurons": 0, | |
| "n_dead_neurons": int(self.som_neuron_count), | |
| "n_total_neurons": int(self.som_neuron_count), | |
| "neuron_activation_rate": 0.0, | |
| "bmu_distribution": {}, | |
| "error": str(e)[:200], | |
| } | |
| def get_hyp_train_steps_used(self) -> Dict[str, Any]: | |
| """V6.5-V2-metrics-FIX-2 — Retorna quantos passos de treino de hipóteses | |
| foram usados até agora. | |
| User requirement: "quantos passos de treino de hipóteses usado". | |
| Returns: | |
| Dict com: | |
| - current_hyp_train_steps: int (valor ATUAL de self.hyp_train_steps) | |
| - total_hyp_train_steps_executed: int (cumulativo ao longo do treino) | |
| - n_train_hypotheses_calls: int (número de chamadas a train_hypotheses) | |
| - min_hyp_train_steps: int (limite inferior dinâmico) | |
| - max_hyp_train_steps: int (limite superior dinâmico) | |
| """ | |
| return { | |
| "current_hyp_train_steps": int(self.hyp_train_steps), | |
| "total_hyp_train_steps_executed": int(getattr(self, "_total_hyp_steps_executed", 0)), | |
| "n_train_hypotheses_calls": int(getattr(self, "_n_train_hyp_calls", 0)), | |
| "min_hyp_train_steps": int(self.min_hyp_train_steps), | |
| "max_hyp_train_steps": int(self.max_hyp_train_steps), | |
| } | |
| # ================================================================== | |
| # V6.5-V2-metrics-FIX-4 — Conscience mechanism + Dead neuron revival | |
| # ================================================================== | |
| # User requirement (latest): "APRIMORAR (em ambas as FASE1 e FASE2): | |
| # analisar matematicamente e logicamente a ativação e uso e acesso dos | |
| # neurônios (apenas dois estão sendo ativados: neurons_active=2/864) | |
| # distribuindo o processamento paralelamente". | |
| # | |
| # Mathematical formulation (Conscience Mechanism — DeSieno 1988): | |
| # | |
| # Cada neurônio i mantém win_frequency p_i ∈ [0,1] (EMA, β=0.1). | |
| # BMU selection: bmu = argmin_i ( ||W_i - x||² - b_i ) | |
| # onde b_i = γ · (1/N - p_i) é o conscience bias | |
| # γ = 0.1 (default), N = total de neurônios (864) | |
| # | |
| # Quando p_i > 1/N (neurônio ganha demais): b_i < 0 → distância | |
| # efetiva AUMENTA → neurônio é penalizado. | |
| # Quando p_i < 1/N (neurônio nunca ganha): b_i > 0 → distância | |
| # efetiva DIMINUI → neurônio é favorecido. | |
| # | |
| # Convergência: p_i → 1/N para todo i (distribuição uniforme de BMU), | |
| # garantindo que TODOS os 864 neurônios sejam utilizados. | |
| # | |
| # Dead neuron revival (complementar): | |
| # Se após K amostras um neurônio ainda tem win_count = 0, ele é | |
| # reinicializado para uma amostra aleatória do buffer (data-driven) | |
| # ou para N(0, 0.1) se buffer vazio. Isto acelera a diversificação | |
| # quando o conscience mechanism sozinho não basta. | |
| # ------------------------------------------------------------------ | |
| def init_weights_kmeans_pp_if_ready(self): | |
| """V6.5-V2-kmeans-pp - Chama init_weights_kmeans_pp no SOM se buffer >= 64. | |
| Tenta uma unica vez durante o treino (idempotente via flag | |
| _kmeans_pp_init_attempted). Se falhar (buffer pequeno, erro), retorna | |
| silently e o SOM mantem a inicializacao grid coords+ruido original. | |
| Returns: | |
| Dict com status da inicializacao. | |
| """ | |
| if getattr(self, "_kmeans_pp_init_attempted", False): | |
| return {"initialized": False, "reason": "already_attempted"} | |
| self._kmeans_pp_init_attempted = True | |
| if len(self.buffer_4d) < self.som._kmeans_pp_min_samples: | |
| return { | |
| "initialized": False, | |
| "reason": f"buffer_too_small ({len(self.buffer_4d)} < {self.som._kmeans_pp_min_samples})", | |
| } | |
| try: | |
| result = self.som.init_weights_kmeans_pp( | |
| data_buffer=self.buffer_4d, | |
| n_iter=5, | |
| random_seed=42, | |
| ) | |
| if result.get("initialized"): | |
| self.som.bmu_win_count.zero_() | |
| target_p = 1.0 / self.som.n_neurons | |
| self.som.win_frequency.fill_(target_p) | |
| self.som._recent_bmu_flat.clear() | |
| return result | |
| except Exception as e: | |
| return {"initialized": False, "reason": f"error: {e}"} | |
| def revive_dead_neurons( | |
| self, | |
| dead_threshold: int = 0, | |
| use_buffer: bool = True, | |
| ) -> Dict[str, Any]: | |
| """V6.5-V2-metrics-FIX-4 — Wrapper KLS para som.revive_dead_neurons. | |
| Usa o buffer_4d atual do KLS como fonte de dados para reinicialização | |
| data-driven dos neurônios mortos. | |
| Args: | |
| dead_threshold: neurônios com win_count <= threshold são revividos. | |
| use_buffer: se True, usa buffer_4d do KLS como fonte. | |
| Returns: | |
| Dict com status do revival (n_revived, dead_rate_before/after, etc). | |
| """ | |
| data_buffer = self.buffer_4d if use_buffer else None | |
| return self.som.revive_dead_neurons( | |
| data_buffer=data_buffer, | |
| dead_threshold=dead_threshold, | |
| ) | |
| def parallel_neuron_activation_report(self) -> Dict[str, Any]: | |
| """V6.5-V2-metrics-FIX-4 — Wrapper KLS para som.parallel_neuron_activation_report. | |
| Retorna relatório estruturado de ativação dos 864 neurônios, análogo | |
| ao DataFrame do exemplo fornecido pelo usuário, mas otimizado para | |
| o grid 4D do SOM. | |
| Returns: | |
| Dict com estatísticas detalhadas (n_active, n_dead, win_count | |
| distribution, conscience_bias stats, uniformity_score, etc). | |
| """ | |
| return self.som.parallel_neuron_activation_report() | |
| # ================================================================== | |
| # V6.5-V2-parallel — Wrappers para novos métodos parallel SOM | |
| # ================================================================== | |
| def train_som_batch_parallel( | |
| self, | |
| samples: torch.Tensor, | |
| use_conscience: bool = True, | |
| accumulate_fisher: bool = False, | |
| ) -> Dict[str, Any]: | |
| """V6.5-V2-parallel — Wrapper KLS para som.train_batch_parallel. | |
| User requirement (ParallelSOM4D pattern): usa paralelismo vetorial | |
| PyTorch ao invés de loops Python + multiprocessing. Todas as operações | |
| internas (distância, BMU, vizinhança, update) são feitas via | |
| broadcasting sobre os 864 neurônios simultaneamente. | |
| Args: | |
| samples: tensor (B, 4) — batch de amostras 4D. | |
| use_conscience: se True, aplica bias de consciência. | |
| accumulate_fisher: se True, acumula Fisher info em w. | |
| Returns: | |
| Dict com: n_processed, n_bmu_unique, dead_rate, alpha_t, sigma_t. | |
| """ | |
| return self.som.train_batch_parallel( | |
| samples=samples, | |
| use_conscience=use_conscience, | |
| accumulate_fisher=accumulate_fisher, | |
| ) | |
| def reorganize_som_map_by_density( | |
| self, | |
| regularization_factor: float = 0.05, | |
| min_dead_rate_for_trigger: float = 0.3, | |
| ) -> Dict[str, Any]: | |
| """V6.5-V2-reorganize — Wrapper KLS para som.reorganize_map_by_density. | |
| User requirement (ReorganizingSOM4D pattern): reorganiza topologia do | |
| SOM dinamicamente durante o treinamento via filtragem de ativação por | |
| densidade. Neurônios pouco ativados (isolados) sofrem força de atração | |
| em direção aos agrupamentos de alta densidade, reorganizando | |
| geometricamente o hipercubo 4D sem perder o alinhamento matricial. | |
| Args: | |
| regularization_factor: magnitude do pull (default 0.05). | |
| min_dead_rate_for_trigger: só reorganiza se dead_rate > threshold. | |
| Returns: | |
| Dict com: reorganized, dead_rate_before, n_dead_pulled, etc. | |
| """ | |
| return self.som.reorganize_map_by_density( | |
| regularization_factor=regularization_factor, | |
| min_dead_rate_for_trigger=min_dead_rate_for_trigger, | |
| ) | |
| def reorganize_map_if_needed( | |
| self, | |
| regularization_factor: float = 0.05, | |
| min_dead_rate_for_trigger: float = 0.3, | |
| min_steps_between_reorganizations: int = 300, | |
| ) -> Dict[str, Any]: | |
| """V6.5-V2-reorganize — Auto-dispara reorganização por densidade se necessário. | |
| Verifica o dead_rate atual e reorganiza o mapa SOM se: | |
| - dead_rate > min_dead_rate_for_trigger (default 0.3 = 30% mortos) | |
| - pelo menos min_steps_between_reorganizations desde a última | |
| reorganização (cooldown) | |
| Complementa auto_revive_if_needed: enquanto auto_revive faz | |
| reinicialização abrupta (k-means++ selection), reorganize_map faz | |
| pulso suave e contínuo (gravitational pull). Ambos podem coexistir. | |
| Args: | |
| regularization_factor: magnitude do pull (default 0.05). | |
| min_dead_rate_for_trigger: limite para disparar (default 0.3). | |
| min_steps_between_reorganizations: cooldown em steps (default 300). | |
| Returns: | |
| Dict com status da reorganização (ou action="skipped" se não disparou). | |
| """ | |
| last_reorg_step = getattr( | |
| self, "_last_reorganize_step", -min_steps_between_reorganizations | |
| ) | |
| current_step = self.som.t | |
| steps_since_last = current_step - last_reorg_step | |
| # Computa dead rate atual | |
| n_total = self.som.n_neurons | |
| n_active = int((self.som.bmu_win_count > 0).sum().item()) | |
| dead_rate = float(1.0 - n_active / max(1, n_total)) | |
| if dead_rate <= min_dead_rate_for_trigger: | |
| return { | |
| "action": "skipped", | |
| "reason": f"dead_rate={dead_rate:.3f} <= threshold={min_dead_rate_for_trigger}", | |
| "dead_rate": dead_rate, | |
| "n_active": n_active, | |
| "n_total": n_total, | |
| } | |
| if steps_since_last < min_steps_between_reorganizations: | |
| return { | |
| "action": "skipped", | |
| "reason": ( | |
| f"cooldown: only {steps_since_last} steps since last reorg " | |
| f"(need {min_steps_between_reorganizations})" | |
| ), | |
| "dead_rate": dead_rate, | |
| "n_active": n_active, | |
| "n_total": n_total, | |
| } | |
| # Dispara reorganização | |
| reorg = self.reorganize_som_map_by_density( | |
| regularization_factor=regularization_factor, | |
| min_dead_rate_for_trigger=min_dead_rate_for_trigger, | |
| ) | |
| self._last_reorganize_step = current_step | |
| reorg["action"] = "auto_reorganized" | |
| reorg["trigger_dead_rate"] = dead_rate | |
| reorg["steps_since_last_reorg"] = steps_since_last | |
| return reorg | |
| def auto_revive_if_needed( | |
| self, | |
| dead_rate_threshold: float = 0.5, | |
| min_steps_between_revivals: int = 200, | |
| ) -> Dict[str, Any]: | |
| """V6.5-V2-metrics-FIX-4 — Revive neurônios automaticamente se dead_rate alto. | |
| Verifica o dead_rate atual e revive neurônios mortos se: | |
| - dead_rate > dead_rate_threshold (default 0.5 = 50% mortos) | |
| - pelo menos min_steps_between_revivals desde o último revival | |
| Isto é chamado automaticamente pelo treinador após cada chunk, | |
| garantindo que o SOM mantenha distribuição uniforme de BMU ao longo | |
| do treino sem intervenção manual. | |
| Args: | |
| dead_rate_threshold: limite para disparar revival (default 0.5). | |
| min_steps_between_revivals: cooldown em steps (default 200). | |
| Returns: | |
| Dict com status do revival (ou action="skipped" se não disparou). | |
| """ | |
| last_revival_step = getattr(self, "_last_revival_step", -min_steps_between_revivals) | |
| current_step = self.som.t | |
| steps_since_last = current_step - last_revival_step | |
| # Computa dead rate atual | |
| report = self.som.parallel_neuron_activation_report() | |
| dead_rate = 1.0 - report["neuron_activation_rate"] | |
| if dead_rate <= dead_rate_threshold: | |
| return { | |
| "action": "skipped", | |
| "reason": f"dead_rate={dead_rate:.3f} <= threshold={dead_rate_threshold}", | |
| "dead_rate": dead_rate, | |
| "n_active": report["n_active_neurons"], | |
| "n_total": report["n_total_neurons"], | |
| } | |
| if steps_since_last < min_steps_between_revivals: | |
| return { | |
| "action": "skipped", | |
| "reason": f"cooldown: only {steps_since_last} steps since last revival " | |
| f"(need {min_steps_between_revivals})", | |
| "dead_rate": dead_rate, | |
| "n_active": report["n_active_neurons"], | |
| "n_total": report["n_total_neurons"], | |
| } | |
| # Dispara revival | |
| revival = self.revive_dead_neurons( | |
| dead_threshold=0, | |
| use_buffer=True, | |
| ) | |
| self._last_revival_step = current_step | |
| revival["action"] = "auto_revived" | |
| revival["trigger_dead_rate"] = dead_rate | |
| revival["steps_since_last_revival"] = steps_since_last | |
| return revival | |
| # ================================================================== | |
| # V6.5-V2-metrics — Integração das métricas SOM canônicas | |
| # (QE, TE, Kaski-Lagus, Variância Explicada, Dead Neurons, | |
| # Colapso Topológico, Estagnação QE, Cruzamento Vizinhança) | |
| # ================================================================== | |
| def compute_som_metrics(self) -> Dict[str, Any]: | |
| """V6.5-V2-metrics — Computa todas as métricas SOM canônicas. | |
| User requirement: "ANALISAR matematicamente e logicamente e inserir | |
| melhorias para os scripts das métricas de aprendizado e indicadores | |
| de falha em Mapas Auto-Organizáveis (SOM / Redes de Kohonen) | |
| avaliam a fidelidade de representação dos dados e a preservação da | |
| vizinhança topológica". | |
| Returns: | |
| Dict com 8 métricas: | |
| 1. quantization_error (QE) | |
| 2. topological_error (TE) | |
| 3. kaski_lagus_error (KL) | |
| 4. explained_variance_share (VE) | |
| 5. topological_collapse (dict) | |
| 6. dead_neuron_rate (dict) | |
| 7. qe_stagnation (dict) — requer histórico | |
| 8. neighborhood_crossing (dict) — requer histórico | |
| Plus: overall_health, failure_indicators | |
| """ | |
| from bigru_t.model.som_metrics import compute_all_metrics | |
| if not self.buffer_4d: | |
| return { | |
| "quantization_error": 0.0, | |
| "topological_error": 0.0, | |
| "kaski_lagus_error": 0.0, | |
| "explained_variance_share": 0.0, | |
| "topological_collapse": {"severity": "none", "effective_rank": 0.0}, | |
| "dead_neuron_rate": {"dead_neuron_rate": 1.0, "n_dead": self.som_neuron_count, | |
| "n_active": 0, "n_total": self.som_neuron_count}, | |
| "qe_stagnation": {"is_stagnant": False, "severity": "none"}, | |
| "neighborhood_crossing": {"detected": False, "severity": "none"}, | |
| "overall_health": "unknown", | |
| "failure_indicators": ["no_buffer_data"], | |
| "n_failure_indicators": 1, | |
| "note": "buffer_4d vazio — métricas indisponíveis", | |
| } | |
| # Stack buffer em tensor (N, 4) | |
| data = torch.stack([ | |
| v.detach().clone() if isinstance(v, torch.Tensor) else torch.tensor(v, dtype=torch.float) | |
| for v in self.buffer_4d | |
| ]).float() | |
| weights = self.som.weights.detach() | |
| metrics = compute_all_metrics(data, weights, self._som_metric_history) | |
| # Registra no histórico | |
| self._som_metric_history.record(metrics) | |
| return metrics | |
| def get_som_metric_history(self) -> Dict[str, Any]: | |
| """Retorna histórico de métricas SOM (para análise temporal).""" | |
| return self._som_metric_history.to_dict() | |
| def reset_som_metric_history(self) -> None: | |
| """Reseta o histórico de métricas SOM (início de nova fase).""" | |
| from bigru_t.model.som_metrics import SOMMetricHistory | |
| self._som_metric_history = SOMMetricHistory() | |
| __all__ = [ | |
| "SimpleBBPETokenizer", | |
| "positional_encoding", | |
| "text_to_4d_vector", | |
| "KohonenSOM4D", | |
| "HypothesisClassifier", | |
| "KohonenLearningSystem", | |
| "DeltaGenerator", | |
| "HypothesisEnsemble", | |
| "KohonenLearningSystemV2", | |
| ] | |
| # V6.5-V2-metrics: som_metrics é importado lazy (dentro de compute_som_metrics) | |
| # para evitar dependência circular e permitir uso standalone. | |