V6.5-V3-hyp-synergy: SynergyHypothesisEnsemble (4 pilares: diversidade k-means++, cooperação, competição, ortogonalidade) + testes de validação (7 files)
80b9167 verified Download src/bigru_t/model/hyp_t.py from PowerMachine/BiGRU_T_version: direct link, hf CLI and curl.
- Browser
- Download file 43 kB
-
https://huggingface.co/PowerMachine/BiGRU_T_version/resolve/main/src/bigru_t/model/hyp_t.py
- Command line
-
hf download hf://PowerMachine/BiGRU_T_version/src/bigru_t/model/hyp_t.py
-
curl -L -o hyp_t.py https://huggingface.co/PowerMachine/BiGRU_T_version/resolve/main/src/bigru_t/model/hyp_t.py
43 kB
| """hyp_t.py — V6.5-V3-hyp-synergy: Cabeça de Hipótese com Sinergia Matemática. | |
| ============================================================================ | |
| VISÃO GERAL (V6.5-V3 — Sinergia entre Hipóteses) | |
| ============================================================================ | |
| User requirement: | |
| "aprimorar matematicamente e logicamente os scripts de hyp_t.py | |
| analisando [...] aprimorar o módulo hyp_t.py para que as hipóteses | |
| (matematicamente e logicamente) trabalhem em sinergia enquanto | |
| treinam e ajustam os parâmetros". | |
| A versão V6.4 do HypT era uma única hipótese global (delta = HypT(o)). | |
| A versão V6.5-V3 eleva o módulo a um ENSEMBLE SINÉRGICO de N_HYPOTHESES | |
| hipóteses paralelas que cooperam e competem durante o treino, em sinergia | |
| com o SOM Kohonen 4D. Os quatro pilares matemáticos da sinergia: | |
| (1) DIVERSIDADE — k-means++ D² scoring na inicialização dos geradores | |
| garante que as N hipóteses cubram regiões distintas | |
| do espaço de deltas desde o início. | |
| (2) COOPERAÇÃO — regularização de consenso: penaliza deltas cuja | |
| direção diverge fortemente do baricentro do ensemble | |
| (Coherence Loss), evitando que hipóteses "vão para | |
| o mato" e se anulem. | |
| (3) COMPETIÇÃO — softmax gating: a cada batch, uma distribuição de | |
| pesos π_h é aprendida sobre as N hipóteses (winner- | |
| takes-more), permitindo que hipóteses promissoras | |
| recebam mais градiente e hipóteses inúteis decaiam. | |
| (4) ORTOGONALIDADE — Gram-Schmidt suave: o conjunto de deltas é projetado | |
| num subespaço quase-ortogonal (penalidade na matriz | |
| de Gram G = D Dᵀ), maximizando a cobertura direcional. | |
| A integração com o SOM Kohonen 4D (256 neurônios em grade (4,4,4,4)) segue o | |
| protocolo de duas fases definido pelo usuário: | |
| FASE1 (CONHECIMENTO) — somente o SOM organiza o mapa Kohonen; HypT NÃO é | |
| ativada (sem ativar layers de hipóteses). | |
| FASE2 (PUNIÇÃO) — SOM ajusta via treinamento + punição ativa; HypT é | |
| treinada para gerar deltas que corrigem o SOM quando | |
| o BMU não representa bem a entrada. | |
| ============================================================================ | |
| MATEMÁTICA DA SINERGIA (V6.5-V3) | |
| ============================================================================ | |
| Seja: | |
| x ∈ ℝ^P_som — ativação SOM da entrada (P_som = 256 para grid (4,4,4,4)) | |
| δ_h ∈ ℝ^P — delta gerado pela hipótese h, h = 1..N | |
| D = [δ_1, …, δ_N] ∈ ℝ^{P × N} — matriz de deltas | |
| π_h ∈ ℝ_+ — peso (responsabilidade) da hipótese h, Σ π_h = 1 | |
| Loss total por batch: | |
| L_total = L_task(D, π) # task loss (BCE/classificação) | |
| + λ_coh · L_coherence(D) # cooperação (consenso) | |
| + λ_orth · L_orth(D) # ortogonalidade (diversidade) | |
| + λ_div · L_diversity(π) # entropia de π (evita colapso) | |
| + λ_l2 · ||D||²_F # regularização L2 (estabilidade) | |
| Onde: | |
| L_coherence = (1/N) · Σ_h ||δ_h − δ̄||² · exp(−‖δ_h‖) # exp evita | |
| # penalizar | |
| # hipóteses ousadas | |
| L_orth = ||G − diag(G)||²_F , G = D̂ D̂ᵀ (D̂ normalizado) | |
| → minimiza correlações entre deltas | |
| L_diversity = −H(π) = Σ_h π_h log π_h (entropy bonus, promove uso | |
| # uniforme das N hipóteses) | |
| Forward (sinergia): | |
| π = softmax(gating(x)) # (N,) — responsabilidades | |
| D = [g_1(x), …, g_N(x)] · delta_scale # (P, N) | |
| δ_ensemble = Σ_h π_h · δ_h # (P,) — delta cooperativo | |
| → δ_ensemble é aplicado ao SOM | |
| O gradiente flui para todos os g_h via δ_ensemble (cooperação) e | |
| também via L_coherence/L_orth (diversidade+ortogonalidade). | |
| ============================================================================ | |
| COMPATIBILIDADE REVERSA | |
| ============================================================================ | |
| A classe `HypT` (single-hypothesis, V6.4) é mantida para scripts legados | |
| que ainda fazem `HypT(...)` e chamam `forward(o, stop_grad=True)`. | |
| Internamente, ela agora delega para `SynergyHypothesisEnsemble` com | |
| n_hypotheses=1 (modo single-hyp), preservando a API antiga: | |
| >>> hyp = HypT(d_input=256, d_model=256, nhead=4, d_ff=512, | |
| ... output_dim=256, num_layers=2, dropout=0.1) | |
| >>> delta = hyp(o, stop_grad=True) # → (batch, output_dim) | |
| A nova API sinérgica é exposta via `SynergyHypothesisEnsemble`: | |
| >>> ens = SynergyHypothesisEnsemble( | |
| ... input_dim=256, output_dim=1024, | |
| ... n_hypotheses=16, max_n_hypotheses=32, | |
| ... hidden_dim=256, | |
| ... lam_coh=0.1, lam_orth=0.05, lam_div=0.01, lam_l2=1e-4, | |
| ... ) | |
| >>> out = ens(x) # → dict com delta_ensemble, deltas, pi, losses | |
| ============================================================================ | |
| AUTOR: V6.5-V3-hyp-synergy | |
| ============================================================================ | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import logging | |
| from dataclasses import dataclass | |
| from typing import Any, Dict, List, Optional, Tuple, Union | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| logger = logging.getLogger(__name__) | |
| # ============================================================================ | |
| # 1. Configuração canônica (NUNCA alterar — diretivas do usuário) | |
| # ============================================================================ | |
| N_HYPOTHESES_CANONICAL: int = 16 | |
| MAX_N_HYPOTHESES_CANONICAL: int = 32 | |
| HYP_TRAIN_STEPS_CANONICAL: int = 30 | |
| HYP_HIDDEN_DIM_CANONICAL: int = 256 | |
| # ============================================================================ | |
| # 2. SynergyConfig — configuração da sinergia | |
| # ============================================================================ | |
| class SynergyConfig: | |
| """Configuração dos pesos das regularizações de sinergia. | |
| Defaults calibrados para P_som=256 (grid (4,4,4,4)) e N=16 hipóteses. | |
| """ | |
| # Pesos das regularizações (todos ≥ 0; 0 desativa) | |
| lam_coh: float = 0.1 # cooperação (consenso de direção) | |
| lam_orth: float = 0.05 # ortogonalidade (diversidade direcional) | |
| lam_div: float = 0.01 # entropia de π (uso uniforme) | |
| lam_l2: float = 1.0e-4 # regularização L2 dos deltas | |
| # Temperature do softmax gating (controla "winner-takes-more") | |
| gating_temperature: float = 1.0 | |
| # Epsilon para estabilidade numérica | |
| eps: float = 1.0e-8 | |
| # Delta scale inicial (pequeno para evitar explosão) | |
| delta_scale_init: float = 0.01 | |
| # Ativar/desativar componentes individualmente | |
| enable_coherence: bool = True | |
| enable_orthogonality: bool = True | |
| enable_diversity: bool = True | |
| enable_l2: bool = True | |
| # OOM-guard: máximo de bytes por tensor de deltas (n_hyp × P × 4) | |
| max_delta_tensor_bytes: int = 256 * 1024 * 1024 # 256 MiB | |
| def to_dict(self) -> Dict[str, Any]: | |
| return { | |
| "lam_coh": float(self.lam_coh), | |
| "lam_orth": float(self.lam_orth), | |
| "lam_div": float(self.lam_div), | |
| "lam_l2": float(self.lam_l2), | |
| "gating_temperature": float(self.gating_temperature), | |
| "eps": float(self.eps), | |
| "delta_scale_init": float(self.delta_scale_init), | |
| "enable_coherence": bool(self.enable_coherence), | |
| "enable_orthogonality": bool(self.enable_orthogonality), | |
| "enable_diversity": bool(self.enable_diversity), | |
| "enable_l2": bool(self.enable_l2), | |
| "max_delta_tensor_bytes": int(self.max_delta_tensor_bytes), | |
| } | |
| # ============================================================================ | |
| # 3. DeltaGenerator — uma hipótese individual | |
| # ============================================================================ | |
| class DeltaGenerator(nn.Module): | |
| """Gerador de delta: g_h(x) → δ_h ∈ ℝ^P. | |
| Arquitetura MLP com 3 camadas (input → hidden → hidden → output). | |
| Inicialização Xavier com gain=0.01 garante deltas pequenos no início. | |
| Sinergia V6.5-V3: usa TransformerEncoder (em vez de MLP puro) quando | |
| `use_transformer=True` (default False — MLP é mais leve em CPU). | |
| Args: | |
| input_dim: dimensão da entrada x (ativação SOM flatten). | |
| output_dim: dimensão do delta (nº de parâmetros do SOM achatados). | |
| hidden_dim: largura das camadas internas (HYP_HIDDEN_DIM=256). | |
| use_transformer: se True, usa TransformerEncoder (mais expressivo, | |
| porém mais custoso). | |
| """ | |
| def __init__( | |
| self, | |
| input_dim: int, | |
| output_dim: int, | |
| hidden_dim: int = HYP_HIDDEN_DIM_CANONICAL, | |
| use_transformer: bool = False, | |
| nhead: int = 4, | |
| num_layers: int = 1, | |
| dropout: float = 0.1, | |
| ): | |
| super().__init__() | |
| self.input_dim = int(input_dim) | |
| self.output_dim = int(output_dim) | |
| self.hidden_dim = int(hidden_dim) | |
| self.use_transformer = bool(use_transformer) | |
| if use_transformer: | |
| # Encoder + proj_out (compatível com HypT V6.4) | |
| self.proj_in = nn.Linear(input_dim, hidden_dim) | |
| encoder_layer = nn.TransformerEncoderLayer( | |
| hidden_dim, nhead, hidden_dim * 4, dropout=dropout, | |
| batch_first=True, | |
| ) | |
| self.encoder = nn.TransformerEncoder(encoder_layer, num_layers) | |
| self.proj_out = nn.Linear(hidden_dim, output_dim) | |
| else: | |
| # MLP leve (default — otimizado para CPU/Xeon) | |
| self.net = nn.Sequential( | |
| nn.Linear(input_dim, hidden_dim), | |
| nn.GELU(), | |
| nn.Linear(hidden_dim, hidden_dim), | |
| nn.GELU(), | |
| nn.Linear(hidden_dim, output_dim), | |
| ) | |
| # Inicialização Xavier com gain=0.01 (deltas pequenos no início) | |
| self._init_weights_xavier_small() | |
| def _init_weights_xavier_small(self) -> None: | |
| """Inicialização Xavier com gain=0.01 para garantir deltas pequenos.""" | |
| for m in self.modules(): | |
| if isinstance(m, nn.Linear): | |
| nn.init.xavier_uniform_(m.weight, gain=0.01) | |
| if m.bias is not None: | |
| nn.init.zeros_(m.bias) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| """x: (batch, input_dim) → δ: (batch, output_dim).""" | |
| if self.use_transformer: | |
| # (batch, 1, input_dim) → encoder → (batch, 1, hidden) → proj | |
| h = self.proj_in(x).unsqueeze(1) # (B, 1, H) | |
| h = self.encoder(h) # (B, 1, H) | |
| return self.proj_out(h.squeeze(1)) # (B, output_dim) | |
| return self.net(x) # (B, output_dim) | |
| # ============================================================================ | |
| # 4. k-means++ Diversidade na Inicialização | |
| # ============================================================================ | |
| def _kmeans_pp_init_delta_directions( | |
| n_hypotheses: int, | |
| output_dim: int, | |
| device: torch.device, | |
| seed: Optional[int] = None, | |
| ) -> torch.Tensor: | |
| """Inicializa N direções quasi-ortogonais usando k-means++ (Arthur & | |
| Vassilvitskii, 2007). | |
| Gera um conjunto de vetores unitários em ℝ^P tais que cada novo vetor | |
| é selecionado com probabilidade proporcional a D(i)² (distância ao | |
| quadrado do vetor mais próximo já escolhido). Isto maximiza a cobertura | |
| angular do conjunto inicial, fornecendo diversidade desde o step 0. | |
| Args: | |
| n_hypotheses: número de direções a gerar. | |
| output_dim: dimensionalidade do espaço (P). | |
| device: dispositivo do tensor retornado. | |
| seed: seed opcional para reprodutibilidade. | |
| Returns: | |
| Tensor (n_hypotheses, output_dim) com direções unitárias. | |
| """ | |
| if seed is not None: | |
| g = torch.Generator(device=device).manual_seed(int(seed)) | |
| else: | |
| g = None | |
| n = max(1, int(n_hypotheses)) | |
| P = int(output_dim) | |
| directions = torch.zeros(n, P, device=device, dtype=torch.float32) | |
| # 1. Primeira direção: aleatória uniforme na esfera unitária P-dim | |
| v0 = torch.randn(P, device=device, generator=g) | |
| v0 = v0 / (v0.norm() + 1.0e-8) | |
| directions[0] = v0 | |
| # 2. Demais direções: k-means++ com D² sampling | |
| for i in range(1, n): | |
| # Distância² de cada candidato (aleatório) ao conjunto já escolhido | |
| # Amostra um pool de candidatos (4× n para diversificar) | |
| n_candidates = max(16, 4 * n) | |
| cand = torch.randn(n_candidates, P, device=device, generator=g) | |
| cand = cand / (cand.norm(dim=-1, keepdim=True) + 1.0e-8) | |
| # D² ao vetor mais próximo já escolhido | |
| # dist²[j, k] = ||cand[j] - directions[k]||² | |
| # = 2 − 2·<cand[j], directions[k]> (unitários) | |
| chosen = directions[:i] # (i, P) | |
| dots = cand @ chosen.t() # (n_cand, i) | |
| dist_sq = (1.0 - dots).clamp(min=0.0).sum(dim=-1) * 2.0 # (n_cand,) | |
| # Probabilidade k-means++: p(j) ∝ D(j)² | |
| probs = dist_sq / (dist_sq.sum() + 1.0e-8) | |
| # Amostra 1 índice segundo probs | |
| idx = torch.multinomial(probs, num_samples=1, generator=g).item() | |
| directions[i] = cand[idx] | |
| return directions | |
| # ============================================================================ | |
| # 5. Gating Network — aprende π (responsabilidades) sobre as N hipóteses | |
| # ============================================================================ | |
| class GatingNetwork(nn.Module): | |
| """Rede de gating: π = softmax(gating(x) / T). | |
| Aprende uma distribuição de responsabilidades π sobre as N hipóteses | |
| com base na entrada x. Inicializada para produzir π ≈ uniforme no | |
| início (sem viés), permitindo que o treino desloque a distribuição | |
| conforme as hipóteses ganham/ perdem utilidade. | |
| Args: | |
| input_dim: dimensão da entrada x. | |
| n_hypotheses: número de hipóteses (= nº de logits de saída). | |
| hidden_dim: largura interna. | |
| temperature: temperatura do softmax (controla sharpness). | |
| """ | |
| def __init__( | |
| self, | |
| input_dim: int, | |
| n_hypotheses: int, | |
| hidden_dim: int = 64, | |
| temperature: float = 1.0, | |
| ): | |
| super().__init__() | |
| self.temperature = float(temperature) | |
| self.net = nn.Sequential( | |
| nn.Linear(input_dim, hidden_dim), | |
| nn.GELU(), | |
| nn.Linear(hidden_dim, n_hypotheses), | |
| ) | |
| # Inicialização zero → logits=0 → π uniforme no início | |
| for m in self.net.modules(): | |
| if isinstance(m, nn.Linear): | |
| nn.init.zeros_(m.weight) | |
| if m.bias is not None: | |
| nn.init.zeros_(m.bias) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| """x: (batch, input_dim) → π: (batch, n_hypotheses), Σ π_h = 1.""" | |
| logits = self.net(x) / max(self.temperature, 1.0e-6) | |
| return F.softmax(logits, dim=-1) | |
| # ============================================================================ | |
| # 6. SynergyHypothesisEnsemble — Ensemble Sinérgico (núcleo do V6.5-V3) | |
| # ============================================================================ | |
| class SynergyHypothesisEnsemble(nn.Module): | |
| """Ensemble sinérgico de N hipóteses para correção delta do SOM. | |
| Implementa os 4 pilares matemáticos da sinergia: | |
| (1) DIVERSIDADE — k-means++ na inicialização das direções | |
| (2) COOPERAÇÃO — coherence loss (consenso de direção) | |
| (3) COMPETIÇÃO — gating softmax com entropia bonus | |
| (4) ORTOGONALIDADE — Gram-Schmidt suave via Frobenius em G = D̂ D̂ᵀ | |
| Args: | |
| input_dim: dimensão da ativação SOM (x). | |
| output_dim: dimensão do delta (P = I*J*K*L*4 para SOM 4D). | |
| n_hypotheses: nº ATIVO inicial de hipóteses (default 16). | |
| max_n_hypotheses: nº MÁXIMO pré-alocado (default 32). | |
| hidden_dim: largura interna dos geradores (default 256). | |
| config: SynergyConfig com pesos das regularizações. | |
| use_transformer: se True, geradores usam TransformerEncoder. | |
| Constantes canônicas (NUNCA alterar): | |
| N_HYPOTHESES = 16, MAX_N_HYPOTHESES = 32, | |
| HYP_TRAIN_STEPS = 30, HYP_HIDDEN_DIM = 256. | |
| """ | |
| def __init__( | |
| self, | |
| input_dim: int, | |
| output_dim: int, | |
| n_hypotheses: int = N_HYPOTHESES_CANONICAL, | |
| max_n_hypotheses: int = MAX_N_HYPOTHESES_CANONICAL, | |
| hidden_dim: int = HYP_HIDDEN_DIM_CANONICAL, | |
| config: Optional[SynergyConfig] = None, | |
| use_transformer: bool = False, | |
| device: Optional[torch.device] = None, | |
| seed: Optional[int] = 42, | |
| ): | |
| super().__init__() | |
| # Sanity checks | |
| if input_dim < 1 or output_dim < 1: | |
| raise ValueError( | |
| f"SynergyHypothesisEnsemble: input_dim={input_dim} e " | |
| f"output_dim={output_dim} devem ser ≥ 1" | |
| ) | |
| n_hypotheses = max(1, min(int(n_hypotheses), int(max_n_hypotheses))) | |
| self.input_dim = int(input_dim) | |
| self.output_dim = int(output_dim) | |
| self.n_hypotheses = int(n_hypotheses) | |
| self.max_n_hypotheses = int(max_n_hypotheses) | |
| self.active_count = int(n_hypotheses) | |
| self.hidden_dim = int(hidden_dim) | |
| self.config = config or SynergyConfig() | |
| self.use_transformer = bool(use_transformer) | |
| self._seed = int(seed) if seed is not None else None | |
| # Pré-aloca max_n_hypotheses geradores | |
| self.generators = nn.ModuleList([ | |
| DeltaGenerator( | |
| input_dim=self.input_dim, | |
| output_dim=self.output_dim, | |
| hidden_dim=self.hidden_dim, | |
| use_transformer=self.use_transformer, | |
| ) | |
| for _ in range(self.max_n_hypotheses) | |
| ]) | |
| # Rede de gating (competição) | |
| self.gating = GatingNetwork( | |
| input_dim=self.input_dim, | |
| n_hypotheses=self.max_n_hypotheses, | |
| hidden_dim=max(32, self.hidden_dim // 4), | |
| temperature=self.config.gating_temperature, | |
| ) | |
| # delta_scale: parâmetro treinável (começa pequeno, cresce conforme | |
| # o treino aprende a usar os deltas) | |
| self.delta_scale = nn.Parameter( | |
| torch.tensor(self.config.delta_scale_init, dtype=torch.float32) | |
| ) | |
| # k-means++ diversity init: aplica direções quasi-ortogonais à | |
| # última camada linear de cada gerador (bias inicial das direções). | |
| self._apply_kmeans_pp_init(device=device or torch.device("cpu")) | |
| # Estado interno (estatísticas para monitoramento) | |
| self._reset_runtime_stats() | |
| # ------------------------------------------------------------------ | |
| # k-means++ init | |
| # ------------------------------------------------------------------ | |
| def _apply_kmeans_pp_init(self, device: torch.device) -> None: | |
| """Aplica direções quasi-ortogonais à última camada de cada gerador. | |
| As direções k-means++ são geradas no espaço ``input_dim`` (espaço de | |
| entrada dos geradores). Cada gerador ``h`` recebe um ``vetor direção`` | |
| distinto quasi-ortogonal aos demais, e esse vetor é ``misturado`` a | |
| uma fração das ``linhas`` da matriz de pesos da camada Linear final | |
| (shape ``(output_dim, input_dim)``). Como cada linha de ``W`` é um | |
| vetor no espaço ``input_dim``, substituir parcialmente essas linhas | |
| pelas direções k-means++ faz com que os ``geradores`` projetem a | |
| entrada em subespaços distintos — produzindo deltas diversificados | |
| desde o início do treino. | |
| """ | |
| try: | |
| # Direções no espaço input_dim (não output_dim!) | |
| directions = _kmeans_pp_init_delta_directions( | |
| n_hypotheses=self.max_n_hypotheses, | |
| output_dim=self.input_dim, | |
| device=device, | |
| seed=self._seed, | |
| ) # (max_n, input_dim) | |
| for h, gen in enumerate(self.generators): | |
| if self.use_transformer: | |
| last_layer = gen.proj_out | |
| else: | |
| # A última camada do nn.Sequential é a Linear final | |
| last_layer = None | |
| for m in gen.net: | |
| if isinstance(m, nn.Linear): | |
| last_layer = m | |
| if last_layer is None: | |
| continue | |
| with torch.no_grad(): | |
| # W: (output_dim, input_dim) — linhas são vetores em input_dim | |
| W = last_layer.weight.data | |
| n_rows = W.shape[0] # = output_dim | |
| # Substitui 10% das linhas pela direção k-means++ do gerador h | |
| n_replace = max(1, n_rows // 10) | |
| idx = torch.randperm(n_rows, device=W.device)[:n_replace] | |
| # Escala pequena para manter deltas pequenos | |
| scale = 0.05 | |
| dir_h = directions[h].to(W.device) # (input_dim,) | |
| # Mistura: W[i] ← scale · (dir_h ⊙ ruído) | |
| for i in idx.tolist(): | |
| noise = torch.randn(W.shape[1], device=W.device) * 0.1 | |
| W[i, :] = scale * (dir_h + noise) | |
| logger.info( | |
| "[SynergyHypothesisEnsemble] k-means++ diversity init applied " | |
| f"to {self.max_n_hypotheses} generators (input_dim=" | |
| f"{self.input_dim}, output_dim={self.output_dim})." | |
| ) | |
| except Exception as e: | |
| logger.warning( | |
| f"[SynergyHypothesisEnsemble] k-means++ init failed: {e}. " | |
| f"Falling back to Xavier-only init." | |
| ) | |
| # ------------------------------------------------------------------ | |
| # Runtime stats | |
| # ------------------------------------------------------------------ | |
| def _reset_runtime_stats(self) -> None: | |
| """Reseta estatísticas internas para monitoramento.""" | |
| self._last_pi: Optional[torch.Tensor] = None | |
| self._last_deltas: Optional[torch.Tensor] = None | |
| self._last_losses: Dict[str, float] = {} | |
| self._forward_count: int = 0 | |
| def set_active_count(self, n: int) -> None: | |
| """Ajusta dinamicamente o número de hipóteses ativas.""" | |
| n = max(1, min(int(n), self.max_n_hypotheses)) | |
| self.active_count = int(n) | |
| self.n_hypotheses = int(n) | |
| # ------------------------------------------------------------------ | |
| # Forward principal (sinergia) | |
| # ------------------------------------------------------------------ | |
| def forward( | |
| self, | |
| x: torch.Tensor, | |
| return_components: bool = True, | |
| stop_grad_x: bool = False, | |
| ) -> Dict[str, torch.Tensor]: | |
| """Forward sinérgico do ensemble. | |
| Args: | |
| x: (batch, input_dim) — ativação SOM da entrada. | |
| return_components: se True, retorna dict com todos os componentes | |
| (deltas, π, perdas). Se False, retorna apenas | |
| {'delta_ensemble': ...}. | |
| stop_grad_x: se True, aplica x.detach() (Lema 3 — isola correção | |
| do gradiente principal). | |
| Returns: | |
| Dict com: | |
| 'delta_ensemble': (batch, output_dim) — delta cooperativo | |
| 'deltas': (batch, n_active, output_dim) — todos os δ_h | |
| 'pi': (batch, n_active) — responsabilidades | |
| 'delta_scale': escalar — escala aplicada | |
| 'losses': dict de regularizações (se return_components) | |
| """ | |
| # OOM-guard: verifica tamanho esperado do tensor de deltas | |
| # (batch × n_active × output_dim × 4 bytes) | |
| est_bytes = ( | |
| x.shape[0] * self.active_count * self.output_dim * 4 | |
| ) | |
| if est_bytes > self.config.max_delta_tensor_bytes: | |
| # Reduz n_active temporariamente para caber no budget | |
| new_n = max( | |
| 1, | |
| int(self.config.max_delta_tensor_bytes | |
| // (x.shape[0] * self.output_dim * 4)), | |
| ) | |
| new_n = min(new_n, self.active_count) | |
| if new_n < self.active_count: | |
| logger.warning( | |
| f"[SynergyHypothesisEnsemble] OOM-guard: reducing " | |
| f"n_active {self.active_count}→{new_n} (est. " | |
| f"{est_bytes/1e6:.1f}MB > budget " | |
| f"{self.config.max_delta_tensor_bytes/1e6:.1f}MB)." | |
| ) | |
| # Ajusta temporariamente (não persistente) | |
| original_active = self.active_count | |
| self.active_count = new_n | |
| else: | |
| original_active = self.active_count | |
| else: | |
| original_active = self.active_count | |
| try: | |
| # Lema 3: stop_grad isola o sinal de correção | |
| if stop_grad_x: | |
| x = x.detach() | |
| batch_size = x.shape[0] | |
| n_active = self.active_count | |
| # 1. Gera N deltas: (batch, n_active, output_dim) | |
| deltas = torch.stack( | |
| [self.generators[h](x) for h in range(n_active)], | |
| dim=1, | |
| ) # (batch, n_active, output_dim) | |
| # 2. Aplica delta_scale (escalar treinável) | |
| scale = self.delta_scale | |
| deltas = scale * deltas | |
| # 3. Gating: π = softmax(gating(x)) → (batch, n_active) | |
| # Pegamos apenas os primeiros n_active logits | |
| pi_full = self.gating(x) # (batch, max_n) | |
| pi = pi_full[:, :n_active] | |
| # Renormaliza π após o slice (já que cortamos os inativos) | |
| pi = pi / (pi.sum(dim=-1, keepdim=True) + self.config.eps) | |
| # 4. Delta ensemble (cooperação): δ_ens = Σ_h π_h · δ_h | |
| # pi: (batch, n_active) → (batch, n_active, 1) | |
| delta_ensemble = (pi.unsqueeze(-1) * deltas).sum(dim=1) # (batch, output_dim) | |
| # 5. Computa regularizações de sinergia | |
| losses: Dict[str, torch.Tensor] = {} | |
| if return_components: | |
| losses = self._compute_synergy_losses(deltas, pi) | |
| # Atualiza stats | |
| self._last_pi = pi.detach() | |
| self._last_deltas = deltas.detach() | |
| self._last_losses = { | |
| k: float(v.item()) if torch.is_tensor(v) else float(v) | |
| for k, v in losses.items() | |
| } | |
| self._forward_count += 1 | |
| result: Dict[str, torch.Tensor] = { | |
| "delta_ensemble": delta_ensemble, | |
| "deltas": deltas, | |
| "pi": pi, | |
| "delta_scale": scale, | |
| } | |
| if return_components: | |
| result["losses"] = losses | |
| return result | |
| finally: | |
| # Restaura active_count se foi temporariamente reduzido | |
| self.active_count = original_active | |
| # ------------------------------------------------------------------ | |
| # Regularizações de sinergia | |
| # ------------------------------------------------------------------ | |
| def _compute_synergy_losses( | |
| self, | |
| deltas: torch.Tensor, # (batch, n_active, output_dim) | |
| pi: torch.Tensor, # (batch, n_active) | |
| ) -> Dict[str, torch.Tensor]: | |
| """Computa as 4 regularizações de sinergia. | |
| L_coherence: penaliza divergência do baricentro (cooperação) | |
| L_orthogonal: penaliza correlação entre deltas (diversidade) | |
| L_diversity: entropia negativa de π (uso uniforme) | |
| L_l2: norma Frobenius de D (estabilidade) | |
| """ | |
| cfg = self.config | |
| eps = cfg.eps | |
| losses: Dict[str, torch.Tensor] = {} | |
| # (1) COOPERAÇÃO — coherence loss | |
| if cfg.enable_coherence and cfg.lam_coh > 0: | |
| # Baricentro do ensemble: δ̄ = (1/N) Σ_h δ_h | |
| delta_bar = deltas.mean(dim=1, keepdim=True) # (batch, 1, output_dim) | |
| # Distância² ao baricentro, com decaimento exponencial para | |
| # não penalizar hipóteses ousadas (||δ_h|| grande) | |
| diff = deltas - delta_bar # (batch, n_active, output_dim) | |
| diff_sq = (diff * diff).sum(dim=-1) # (batch, n_active) | |
| # Peso exponencial: w_h = exp(-||δ_h||) | |
| delta_norm = deltas.norm(dim=-1) # (batch, n_active) | |
| w = torch.exp(-delta_norm) # (batch, n_active) | |
| # L_coh = mean_{b,h} w_{b,h} · ||δ_h - δ̄||² | |
| l_coh_raw = (w * diff_sq).mean() | |
| losses["loss_coherence"] = cfg.lam_coh * l_coh_raw | |
| else: | |
| losses["loss_coherence"] = torch.tensor(0.0, device=deltas.device) | |
| # (2) ORTOGONALIDADE — Gram-Schmidt suave | |
| if cfg.enable_orthogonality and cfg.lam_orth > 0: | |
| # Normaliza deltas para o cálculo de G (estabilidade) | |
| d_norm = deltas.norm(dim=-1, keepdim=True).clamp(min=eps) | |
| d_hat = deltas / d_norm # (batch, n_active, output_dim) — unitários | |
| # G = D̂ D̂ᵀ ∈ ℝ^{n_active × n_active} (matriz de Gram por batch) | |
| G = torch.matmul(d_hat, d_hat.transpose(1, 2)) # (batch, n_active, n_active) | |
| # Off-diagonal: penaliza correlações | |
| I = torch.eye(G.shape[1], device=G.device).unsqueeze(0) | |
| off_diag = G - I | |
| l_orth_raw = (off_diag * off_diag).sum(dim=(-1, -2)).mean() | |
| losses["loss_orthogonality"] = cfg.lam_orth * l_orth_raw | |
| else: | |
| losses["loss_orthogonality"] = torch.tensor(0.0, device=deltas.device) | |
| # (3) DIVERSIDADE — entropia de π (entropia bonus) | |
| if cfg.enable_diversity and cfg.lam_div > 0: | |
| # H(π) = -Σ π log π → maximizar H = minimizar -H | |
| log_pi = torch.log(pi.clamp(min=eps)) | |
| entropy = -(pi * log_pi).sum(dim=-1) # (batch,) | |
| # log(N) é a entropia máxima (distribuição uniforme) | |
| log_n = math.log(max(self.active_count, 1)) | |
| # Normaliza: l_div ∈ [0, 1] (0 = uniforme, 1 = colapsada) | |
| l_div_raw = (1.0 - entropy / max(log_n, eps)).mean() | |
| losses["loss_diversity"] = cfg.lam_div * l_div_raw | |
| else: | |
| losses["loss_diversity"] = torch.tensor(0.0, device=deltas.device) | |
| # (4) L2 — norma Frobenius de D | |
| if cfg.enable_l2 and cfg.lam_l2 > 0: | |
| l_l2_raw = (deltas * deltas).sum() | |
| losses["loss_l2"] = cfg.lam_l2 * l_l2_raw | |
| else: | |
| losses["loss_l2"] = torch.tensor(0.0, device=deltas.device) | |
| # Total | |
| losses["loss_synergy_total"] = ( | |
| losses["loss_coherence"] | |
| + losses["loss_orthogonality"] | |
| + losses["loss_diversity"] | |
| + losses["loss_l2"] | |
| ) | |
| return losses | |
| # ------------------------------------------------------------------ | |
| # API utilitária para o trainer | |
| # ------------------------------------------------------------------ | |
| def forward_stacked(self, x: torch.Tensor) -> torch.Tensor: | |
| """Compat com HypothesisEnsemble.forward_stacked (legado). | |
| Retorna (batch, n_active, output_dim) — apenas os deltas, sem π. | |
| """ | |
| out = self.forward(x, return_components=False, stop_grad_x=False) | |
| return out["deltas"] | |
| def get_delta_ensemble( | |
| self, x: torch.Tensor, stop_grad_x: bool = False | |
| ) -> torch.Tensor: | |
| """Atalho para obter apenas o delta cooperativo.""" | |
| return self.forward( | |
| x, return_components=False, stop_grad_x=stop_grad_x | |
| )["delta_ensemble"] | |
| def get_last_stats(self) -> Dict[str, Any]: | |
| """Retorna estatísticas do último forward (para monitoramento).""" | |
| pi = self._last_pi | |
| deltas = self._last_deltas | |
| stats: Dict[str, Any] = { | |
| "forward_count": int(self._forward_count), | |
| "active_count": int(self.active_count), | |
| "delta_scale": float(self.delta_scale.item()), | |
| "losses": dict(self._last_losses), | |
| } | |
| if pi is not None: | |
| # Entropia normalizada de π (1.0 = uniforme, 0.0 = colapsada) | |
| eps = self.config.eps | |
| log_pi = torch.log(pi.clamp(min=eps)) | |
| entropy = -(pi * log_pi).sum(dim=-1).mean().item() | |
| log_n = math.log(max(self.active_count, 1)) | |
| stats["pi_entropy"] = float(entropy) | |
| stats["pi_normalized_entropy"] = float(entropy / max(log_n, eps)) | |
| stats["pi_max"] = float(pi.max(dim=-1).values.mean().item()) | |
| stats["pi_min"] = float(pi.min(dim=-1).values.mean().item()) | |
| if deltas is not None: | |
| # Norma média dos deltas | |
| stats["delta_norm_mean"] = float( | |
| deltas.norm(dim=-1).mean().item() | |
| ) | |
| # Diversidade: 1 - cos(δ_i, δ_j) médio | |
| if deltas.shape[1] >= 2: | |
| d_norm = deltas.norm(dim=-1, keepdim=True).clamp(min=eps) | |
| d_hat = deltas / d_norm | |
| G = torch.matmul(d_hat, d_hat.transpose(1, 2)) | |
| n = G.shape[1] | |
| # Off-diagonal mean | |
| mask = ~torch.eye(n, dtype=torch.bool, device=G.device) | |
| off_mean = float(G[:, mask].mean().item()) | |
| stats["delta_correlation_mean"] = off_mean # ideal: ~0 | |
| return stats | |
| # ============================================================================ | |
| # 7. HypT — Wrapper legado (compatibilidade reversa com V6.4) | |
| # ============================================================================ | |
| class HypT(nn.Module): | |
| """HypT (V6.4 compat) — wrapper para SynergyHypothesisEnsemble em modo | |
| single-hypothesis. | |
| Mantém a API original para scripts legados: | |
| >>> hyp = HypT(d_input=256, d_model=256, nhead=4, d_ff=512, | |
| ... output_dim=256, num_layers=2, dropout=0.1) | |
| >>> delta = hyp(o, stop_grad=True) # → (batch, output_dim) | |
| Internamente, delega para SynergyHypothesisEnsemble com n_hypotheses=1 | |
| (modo single-hyp, sem regularizações de sinergia — apenas L2 leve). | |
| Args: | |
| d_input: dimensão de entrada (saída do OrqCell = d_cache). | |
| d_model: dimensão interna (mantido para compat, mapeado para hidden_dim). | |
| nhead: nº de cabeças do Transformer (mantido para compat). | |
| d_ff: dimensão do FFN (mantido para compat, ignorado no modo MLP). | |
| output_dim: dimensão de saída (igual ao TrainT). | |
| num_layers: nº de camadas (mantido para compat). | |
| dropout: probabilidade de dropout. | |
| Forward: | |
| o: (batch, d_input), stop_grad: bool → delta: (batch, output_dim) | |
| """ | |
| def __init__( | |
| self, | |
| d_input: int, | |
| d_model: int, | |
| nhead: int, | |
| d_ff: int, | |
| output_dim: int, | |
| num_layers: int = 2, | |
| dropout: float = 0.1, | |
| ): | |
| super().__init__() | |
| # Config single-hyp: sem sinergia (apenas L2 leve para estabilidade) | |
| cfg = SynergyConfig( | |
| lam_coh=0.0, | |
| lam_orth=0.0, | |
| lam_div=0.0, | |
| lam_l2=1.0e-4, | |
| enable_coherence=False, | |
| enable_orthogonality=False, | |
| enable_diversity=False, | |
| enable_l2=True, | |
| delta_scale_init=1.0, # scale=1 para preservar magnitude V6.4 | |
| ) | |
| self._ensemble = SynergyHypothesisEnsemble( | |
| input_dim=int(d_input), | |
| output_dim=int(output_dim), | |
| n_hypotheses=1, | |
| max_n_hypotheses=1, | |
| hidden_dim=int(d_model) if d_model else HYP_HIDDEN_DIM_CANONICAL, | |
| config=cfg, | |
| use_transformer=True, # Mantém TransformerEncoder para compat V6.4 | |
| ) | |
| # Guarda parâmetros originais para introspecção | |
| self.d_input = int(d_input) | |
| self.d_model = int(d_model) | |
| self.output_dim = int(output_dim) | |
| self.nhead = int(nhead) | |
| self.d_ff = int(d_ff) | |
| self.num_layers = int(num_layers) | |
| self.dropout = float(dropout) | |
| def forward( | |
| self, o: torch.Tensor, stop_grad: bool = True | |
| ) -> torch.Tensor: | |
| """o: (batch, d_input) → delta: (batch, output_dim). | |
| V6.5-V3: delega para SynergyHypothesisEnsemble (single-hyp mode). | |
| """ | |
| out = self._ensemble.forward( | |
| o, return_components=False, stop_grad_x=stop_grad | |
| ) | |
| # delta_ensemble = Σ_h π_h · δ_h, com n_hyp=1 → π_1=1, δ_ens=δ_1 | |
| return out["delta_ensemble"] | |
| # ------------------------------------------------------------------ | |
| # Exposição de estado interno (para inspeção/depuração) | |
| # ------------------------------------------------------------------ | |
| def get_ensemble(self) -> SynergyHypothesisEnsemble: | |
| """Retorna o ensemble interno (para upgrade sinérgico posterior).""" | |
| return self._ensemble | |
| # ============================================================================ | |
| # 8. Factory helpers | |
| # ============================================================================ | |
| def create_synergy_ensemble( | |
| input_dim: int, | |
| output_dim: int, | |
| n_hypotheses: int = N_HYPOTHESES_CANONICAL, | |
| max_n_hypotheses: int = MAX_N_HYPOTHESES_CANONICAL, | |
| hidden_dim: int = HYP_HIDDEN_DIM_CANONICAL, | |
| config: Optional[SynergyConfig] = None, | |
| use_transformer: bool = False, | |
| device: Optional[torch.device] = None, | |
| seed: Optional[int] = 42, | |
| ) -> SynergyHypothesisEnsemble: | |
| """Factory para criar um SynergyHypothesisEnsemble com defaults canônicos. | |
| Constantes canônicas (NUNCA alterar): | |
| N_HYPOTHESES = 16, MAX_N_HYPOTHESES = 32, | |
| HYP_TRAIN_STEPS = 30, HYP_HIDDEN_DIM = 256. | |
| """ | |
| return SynergyHypothesisEnsemble( | |
| input_dim=input_dim, | |
| output_dim=output_dim, | |
| n_hypotheses=n_hypotheses, | |
| max_n_hypotheses=max_n_hypotheses, | |
| hidden_dim=hidden_dim, | |
| config=config, | |
| use_transformer=use_transformer, | |
| device=device, | |
| seed=seed, | |
| ) | |
| # ============================================================================ | |
| # 9. Self-test (executável: python -m bigru_t.model.hyp_t) | |
| # ============================================================================ | |
| def _self_test() -> Dict[str, Any]: | |
| """Self-test do módulo: cria ensemble, faz forward, verifica saídas.""" | |
| print("[hyp_t self-test] Starting...") | |
| torch.manual_seed(42) | |
| # Parâmetros canônicos | |
| P_som = 256 # grid (4,4,4,4) flatten | |
| output_dim = 256 * 4 # I*J*K*L*4 = 256*4 = 1024 | |
| n_hyp = N_HYPOTHESES_CANONICAL | |
| max_n = MAX_N_HYPOTHESES_CANONICAL | |
| hidden = HYP_HIDDEN_DIM_CANONICAL | |
| # Cria ensemble | |
| ens = create_synergy_ensemble( | |
| input_dim=P_som, | |
| output_dim=output_dim, | |
| n_hypotheses=n_hyp, | |
| max_n_hypotheses=max_n, | |
| hidden_dim=hidden, | |
| use_transformer=False, # MLP para self-test rápido | |
| device=torch.device("cpu"), | |
| seed=42, | |
| ) | |
| print(f" Ensemble criado: {n_hyp} hipóteses (max {max_n}), " | |
| f"hidden={hidden}, P_som={P_som}, output_dim={output_dim}") | |
| # Forward com batch de 8 amostras | |
| x = torch.randn(8, P_som) | |
| out = ens(x) | |
| delta_ens = out["delta_ensemble"] | |
| deltas = out["deltas"] | |
| pi = out["pi"] | |
| losses = out["losses"] | |
| # Verifica shapes | |
| assert delta_ens.shape == (8, output_dim), \ | |
| f"delta_ensemble shape: {delta_ens.shape} != (8, {output_dim})" | |
| assert deltas.shape == (8, n_hyp, output_dim), \ | |
| f"deltas shape: {deltas.shape} != (8, {n_hyp}, {output_dim})" | |
| assert pi.shape == (8, n_hyp), \ | |
| f"pi shape: {pi.shape} != (8, {n_hyp})" | |
| # Verifica que π soma 1 | |
| pi_sum = pi.sum(dim=-1) | |
| assert torch.allclose(pi_sum, torch.ones(8), atol=1e-5), \ | |
| f"π não soma 1: {pi_sum}" | |
| # Verifica losses | |
| print(f" Losses: " + ", ".join( | |
| f"{k}={v.item():.6f}" for k, v in losses.items() | |
| )) | |
| assert torch.isfinite(losses["loss_synergy_total"]), \ | |
| "loss_synergy_total não é finita" | |
| # Stats | |
| stats = ens.get_last_stats() | |
| print(f" Stats: pi_entropy={stats.get('pi_entropy', 0):.4f} " | |
| f"(norm={stats.get('pi_normalized_entropy', 0):.4f}), " | |
| f"delta_norm_mean={stats.get('delta_norm_mean', 0):.6f}, " | |
| f"corr={stats.get('delta_correlation_mean', 0):.4f}") | |
| # Test HypT legacy wrapper | |
| hyp = HypT( | |
| d_input=P_som, d_model=hidden, nhead=4, d_ff=hidden * 2, | |
| output_dim=output_dim, num_layers=1, dropout=0.1, | |
| ) | |
| delta_legacy = hyp(x, stop_grad=True) | |
| assert delta_legacy.shape == (8, output_dim), \ | |
| f"HypT legacy delta shape: {delta_legacy.shape}" | |
| assert torch.isfinite(delta_legacy).all(), "HypT legacy delta não finito" | |
| print(f" HypT legacy OK: delta shape={delta_legacy.shape}, " | |
| f"norm_mean={delta_legacy.norm(dim=-1).mean().item():.6f}") | |
| print("[hyp_t self-test] ALL CHECKS PASSED ✓") | |
| return { | |
| "ensemble_n_hyp": n_hyp, | |
| "ensemble_max_n": max_n, | |
| "ensemble_hidden": hidden, | |
| "delta_ensemble_shape": list(delta_ens.shape), | |
| "deltas_shape": list(deltas.shape), | |
| "pi_shape": list(pi.shape), | |
| "pi_sum_mean": float(pi_sum.mean().item()), | |
| "losses": {k: float(v.item()) for k, v in losses.items()}, | |
| "stats": stats, | |
| "hyp_legacy_shape": list(delta_legacy.shape), | |
| } | |
| # ============================================================================ | |
| # __all__ | |
| # ============================================================================ | |
| __all__ = [ | |
| # Config | |
| "SynergyConfig", | |
| # Classes principais | |
| "SynergyHypothesisEnsemble", | |
| "DeltaGenerator", | |
| "GatingNetwork", | |
| "HypT", # legacy wrapper (V6.4 compat) | |
| # Constantes canônicas | |
| "N_HYPOTHESES_CANONICAL", | |
| "MAX_N_HYPOTHESES_CANONICAL", | |
| "HYP_TRAIN_STEPS_CANONICAL", | |
| "HYP_HIDDEN_DIM_CANONICAL", | |
| # Factory | |
| "create_synergy_ensemble", | |
| # k-means++ init | |
| "_kmeans_pp_init_delta_directions", | |
| ] | |
| if __name__ == "__main__": | |
| _self_test() | |