Delete src/bigru_t/model/orq_cell.py
Browse files- src/bigru_t/model/orq_cell.py +0 -738
src/bigru_t/model/orq_cell.py
DELETED
|
@@ -1,738 +0,0 @@
|
|
| 1 |
-
"""orq_cell.py — V6: OrqCell Kohonen 4D Self-Organizing Map (SOM).
|
| 2 |
-
|
| 3 |
-
═══════════════════════════════════════════════════════════════════════════════
|
| 4 |
-
V6 — KOHONEN 4D SELF-ORGANIZING MAP
|
| 5 |
-
═══════════════════════════════════════════════════════════════════════════════
|
| 6 |
-
|
| 7 |
-
Reescrito completamente em V6 como um SOM 4D (Kohonen). A camada OrqCell agora
|
| 8 |
-
é o ORQUESTRADOR AUTO-ORGANIZÁVEL do BiGRU_T — semelhante ao PGVector em
|
| 9 |
-
espírito (busca por similaridade), mas que aprende a topologia dos dados ao
|
| 10 |
-
longo do treinamento, em vez de ser uma simples memória vetorial estática.
|
| 11 |
-
|
| 12 |
-
Mapeamento arquitetural (definido pelo usuário):
|
| 13 |
-
|
| 14 |
-
┌─────────────────────────────────────────────────────────────────────────┐
|
| 15 |
-
│ Componente SOM │ Implementação BiGRU_T │
|
| 16 |
-
├─────────────────────────────────────────────────────────────────────────┤
|
| 17 |
-
│ Neurônios do mapa (8) │ 8 BiGRU4 paralelas dentro de cada │
|
| 18 |
-
│ │ u8cell_T — cada BiGRU4 tem peso próprio │
|
| 19 |
-
│ Pesos dos neurônios │ Parâmetros aprendíveis (cache + proj_in │
|
| 20 |
-
│ │ + proj_out + TransformerUnit) │
|
| 21 |
-
│ Dimensão semântica │ TransformerUnit (atenção = função de │
|
| 22 |
-
│ (função de vizinhança) │ vizinhança aprendível) │
|
| 23 |
-
│ Mecanismo de atualização │ Best-Matching Unit (BMU) + vizinhos │
|
| 24 |
-
│ │ puxados na direção do vetor de entrada │
|
| 25 |
-
│ Preservação de topologia │ Função Gaussiana de decaimento │
|
| 26 |
-
│ │ Λ(d, σ) = exp(-d²/(2σ²)) │
|
| 27 |
-
│ Raio de vizinhança σ │ Decai ao longo do treino (sigma_t = │
|
| 28 |
-
│ │ sigma_0 * (sigma_f/sigma_0)^(t/T)) │
|
| 29 |
-
│ Taxa de aprendizado α │ Decai ao longo do treino (alpha_t = │
|
| 30 |
-
│ │ alpha_0 * (alpha_f/alpha_0)^(t/T)) │
|
| 31 |
-
│ Malha 4D │ 8 BiGRU4 dispostas em grid 2x2x2x2 │
|
| 32 |
-
│ │ ( coordenadas (i,j,k,l), i,j,k,l∈{0,1}) │
|
| 33 |
-
└─────────────────────────────────────────────────────────────────────────┘
|
| 34 |
-
|
| 35 |
-
COMO FUNCIONA
|
| 36 |
-
─────────────
|
| 37 |
-
|
| 38 |
-
1. Para cada vetor de entrada x (saída agregada dos u8cell_T), os 8 neurônios
|
| 39 |
-
BiGRU4 produzem 8 representações internas.
|
| 40 |
-
2. O TransformerUnit (dimensão semântica) calcula scores de similaridade
|
| 41 |
-
aprendíveis entre x e cada neurônio.
|
| 42 |
-
3. O neurônio com maior score é o BMU (Best-Matching Unit).
|
| 43 |
-
4. A função Gaussiana Λ(d_bmu, σ) pondera cada neurônio pela distância 4D até
|
| 44 |
-
o BMU no grid 2x2x2x2.
|
| 45 |
-
5. Os pesos dos neurônios são atualizados (em treino) na direção de x:
|
| 46 |
-
w_i ← w_i + α · Λ(d_i_bmu, σ) · (x - w_i)
|
| 47 |
-
onde α é a taxa de aprendizado e σ é o raio de vizinhança atuais.
|
| 48 |
-
6. Em EVAL mode, o mapa é congelado — apenas a busca BMU é executada.
|
| 49 |
-
7. A saída agregada combina:
|
| 50 |
-
- A representação do BMU (classe vencedora)
|
| 51 |
-
- Média ponderada por Λ (que preserva topologia)
|
| 52 |
-
- Cache aprendível cross-attention (compatibilidade V5)
|
| 53 |
-
|
| 54 |
-
COMO LIDA COM DADOS NÃO-PREPARADOS PARA KOHONEN 4D
|
| 55 |
-
──────────────────────────────────────────────────
|
| 56 |
-
|
| 57 |
-
O usuário observou que os datasets não são preparados para essa arquitetura
|
| 58 |
-
Kohonen 4D. Portanto:
|
| 59 |
-
|
| 60 |
-
a) Toda vez que um tensor/vetor de entrada NÃO tem a dimensão esperada para
|
| 61 |
-
um neurônio BiGRU4 (semântica + pesos), o OrqCell atribui valores
|
| 62 |
-
aleatórios aos parâmetros faltantes e prossegue. Isto é feito via
|
| 63 |
-
`_ensure_neuron_compat()` que verifica shape em runtime e faz pad/resize.
|
| 64 |
-
b) Em val mode (ou quando há punição), o OrqCell primeiro consulta o mapa
|
| 65 |
-
para ver se a informação já está presente (`map_contains_info()`).
|
| 66 |
-
Se não houver, atribui valores aleatórios controlados aos parâmetros
|
| 67 |
-
faltantes (magnitudes pequenas para não perturbar o mapa já treinado).
|
| 68 |
-
c) O BMU e os vizinhos são atualizados apenas em train mode. Em eval, o
|
| 69 |
-
mapa �� read-only.
|
| 70 |
-
|
| 71 |
-
MONITORAMENTO V6
|
| 72 |
-
────────────────
|
| 73 |
-
|
| 74 |
-
A cada step de treino, expõe para o MetricsMonitor:
|
| 75 |
-
- sigma_t: raio de vizinhança atual
|
| 76 |
-
- alpha_t: taxa de aprendizado atual
|
| 77 |
-
- bmu_id: índice do neurônio vencedor (média do batch)
|
| 78 |
-
- bmu_diversity: nº de BMUs distintos no batch (alta = map cobrindo bem)
|
| 79 |
-
- neighborhood_spread: média ponderada de Λ (alta = update amplo)
|
| 80 |
-
- map_entropy: entropia da distribuição de BMUs (alta = uso balanceado)
|
| 81 |
-
"""
|
| 82 |
-
from __future__ import annotations
|
| 83 |
-
|
| 84 |
-
import math
|
| 85 |
-
from dataclasses import dataclass, field
|
| 86 |
-
from typing import Optional, Tuple, List, Dict
|
| 87 |
-
|
| 88 |
-
import torch
|
| 89 |
-
import torch.nn as nn
|
| 90 |
-
import torch.nn.functional as F
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
# ============================================================================
|
| 94 |
-
# Config
|
| 95 |
-
# ============================================================================
|
| 96 |
-
|
| 97 |
-
@dataclass
|
| 98 |
-
class Kohonen4DConfig:
|
| 99 |
-
"""Configuração do SOM 4D Kohonen.
|
| 100 |
-
|
| 101 |
-
Defaults conservadores para treinamento de 100 samples em CPU.
|
| 102 |
-
"""
|
| 103 |
-
# Dimensões
|
| 104 |
-
n_neurons: int = 8 # 8 neurônios em arranjo 4D
|
| 105 |
-
# grid_shape (2,2,2,1): 8 células em espaço 4D (4ª dimensão degenerada
|
| 106 |
-
# — apenas para permitir coordenação 4D; pode ser estendida para (2,2,2,2)
|
| 107 |
-
# com 16 neurônios no futuro, quando max_modules subir de 8 para 16).
|
| 108 |
-
grid_shape: Tuple[int, int, int, int] = (2, 2, 2, 1)
|
| 109 |
-
d_cache: int = 128 # dimensão do cache aprendível
|
| 110 |
-
d_model_in: int = 128 # dimensão da entrada (saída u8cell_T)
|
| 111 |
-
nhead: int = 4
|
| 112 |
-
d_ff: int = 256
|
| 113 |
-
dropout: float = 0.1
|
| 114 |
-
|
| 115 |
-
# Schedule do SOM
|
| 116 |
-
sigma_0: float = 1.5 # raio inicial (cobre ~3 vizinhos no grid 2x2x2x2)
|
| 117 |
-
sigma_f: float = 0.3 # raio final (apenas BMU + vizinho imediato)
|
| 118 |
-
alpha_0: float = 0.5 # taxa de aprendizado inicial
|
| 119 |
-
alpha_f: float = 0.05 # taxa de aprendizado final
|
| 120 |
-
total_steps: int = 200 # T (passos para decair sigma_0→sigma_f)
|
| 121 |
-
|
| 122 |
-
# Controle de atualização
|
| 123 |
-
update_neighbors_in_eval: bool = False # nunca atualizar em eval
|
| 124 |
-
random_pad_scale: float = 0.02 # escala para valores aleatórios de parâmetros faltantes
|
| 125 |
-
bmu_temp: float = 1.0 # temperatura do softmax de seleção de BMU
|
| 126 |
-
|
| 127 |
-
# ───────────────────────────────────────────────────────────────────────
|
| 128 |
-
# V6.1 — PGVector-like lookup thresholds
|
| 129 |
-
# ───────────────────────────────────────────────────────────────────────
|
| 130 |
-
# Quando o usuário treina com datasets não preparados para Kohonen 4D,
|
| 131 |
-
# o mapa precisa responder a uma pergunta: "este vetor de entrada já
|
| 132 |
-
# está representado no SOM?" A resposta pode ser:
|
| 133 |
-
# - COMPLETE : existe um neurônio com similaridade alta (≥ pgvector_complete_thr)
|
| 134 |
-
# E esse neurônio está "filled" (já recebeu update SOM)
|
| 135 |
-
# - INCOMPLETE : existe um neurônio parecido, mas NÃO está filled
|
| 136 |
-
# (faltam elementos — os pesos EWC ainda não foram computados)
|
| 137 |
-
# - MISSING : nenhum neurônio tem similaridade ≥ pgvector_missing_thr
|
| 138 |
-
# (o mapa não tem aquela info de forma alguma)
|
| 139 |
-
pgvector_complete_thr: float = 0.6 # sim ≥ thr → COMPLETE
|
| 140 |
-
pgvector_missing_thr: float = 0.2 # sim < thr → MISSING
|
| 141 |
-
# (entre missing_thr e complete_thr → INCOMPLETE)
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
# ============================================================================
|
| 145 |
-
# Coordenadas 4D do grid 2x2x2x2
|
| 146 |
-
# ============================================================================
|
| 147 |
-
|
| 148 |
-
def _build_grid_coords(grid_shape: Tuple[int, int, int, int]) -> torch.Tensor:
|
| 149 |
-
"""Constrói as coordenadas 4D de cada neurônio no grid.
|
| 150 |
-
|
| 151 |
-
Returns: (n_neurons, 4) — coordenadas inteiras de cada neurônio.
|
| 152 |
-
Para grid (2,2,2,2): 8 coordenadas [(i,j,k,l) for i,j,k,l in {0,1}]
|
| 153 |
-
"""
|
| 154 |
-
coords = []
|
| 155 |
-
for i in range(grid_shape[0]):
|
| 156 |
-
for j in range(grid_shape[1]):
|
| 157 |
-
for k in range(grid_shape[2]):
|
| 158 |
-
for l in range(grid_shape[3]):
|
| 159 |
-
coords.append([i, j, k, l])
|
| 160 |
-
return torch.tensor(coords, dtype=torch.float32) # (n_neurons, 4)
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
def _pairwise_grid_distances(coords: torch.Tensor) -> torch.Tensor:
|
| 164 |
-
"""Distância euclidiana 4D entre todos os pares de neurônios.
|
| 165 |
-
|
| 166 |
-
Args: coords (n_neurons, 4)
|
| 167 |
-
Returns: (n_neurons, n_neurons) — dist[i,j] = ||coords[i] - coords[j]||_2
|
| 168 |
-
"""
|
| 169 |
-
# ||c_i - c_j||^2 = ||c_i||^2 + ||c_j||^2 - 2 c_i·c_j
|
| 170 |
-
sq = (coords ** 2).sum(dim=-1) # (n,)
|
| 171 |
-
d2 = sq.unsqueeze(0) + sq.unsqueeze(1) - 2 * coords @ coords.T
|
| 172 |
-
return torch.sqrt(d2.clamp(min=0.0) + 1e-12)
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
# ============================================================================
|
| 176 |
-
# OrqCell V6 — Kohonen 4D SOM
|
| 177 |
-
# ============================================================================
|
| 178 |
-
|
| 179 |
-
class OrqCell(nn.Module):
|
| 180 |
-
"""Orquestrador com SOM 4D Kohonen (V6).
|
| 181 |
-
|
| 182 |
-
Substitui o OrqCell V5 (que era cross-attention puro) por um mapa
|
| 183 |
-
auto-organizável onde os 8 neurônios são as 8 saídas dos BiGRU4 já
|
| 184 |
-
processados pelo u8cell_T, e o TransformerUnit atua como dimensão
|
| 185 |
-
semântica aprendível que substitui a distância euclidiana clássica.
|
| 186 |
-
|
| 187 |
-
Args:
|
| 188 |
-
config: Kohonen4DConfig
|
| 189 |
-
|
| 190 |
-
Forward (train mode):
|
| 191 |
-
H: (batch, K, d_model_in) — K saídas dos K módulos u8cell_T
|
| 192 |
-
step: int — passo global de treino (para schedule de σ e α)
|
| 193 |
-
→ o: (batch, d_cache)
|
| 194 |
-
→ aux_dict com sigma_t, alpha_t, bmu_id, etc.
|
| 195 |
-
|
| 196 |
-
Forward (eval mode):
|
| 197 |
-
Mesma assinatura, mas sem atualizar pesos do mapa.
|
| 198 |
-
"""
|
| 199 |
-
def __init__(self, config: Optional[Kohonen4DConfig] = None, **kwargs):
|
| 200 |
-
"""Inicializa o OrqCell V6 Kohonen 4D SOM.
|
| 201 |
-
|
| 202 |
-
Backward-compatible com V5 API: aceita kwargs (cache_len, d_cache,
|
| 203 |
-
d_model_in, nhead, d_ff, dropout) e converte para Kohonen4DConfig.
|
| 204 |
-
Se `config` for fornecido, tem prioridade.
|
| 205 |
-
"""
|
| 206 |
-
super().__init__()
|
| 207 |
-
# Se config não fornecido, construir a partir de kwargs (V5 compat)
|
| 208 |
-
# V6 always uses 8 neurons regardless of cache_len (cache_len is ignored
|
| 209 |
-
# — V5 used 16 positions but V6 Kohonen requires exactly 8 neurons
|
| 210 |
-
# arranged in 4D grid (2,2,2,1)).
|
| 211 |
-
if config is None:
|
| 212 |
-
d_cache = kwargs.get("d_cache", 128)
|
| 213 |
-
d_model_in = kwargs.get("d_model_in", 128)
|
| 214 |
-
nhead = kwargs.get("nhead", 4)
|
| 215 |
-
d_ff = kwargs.get("d_ff", 256)
|
| 216 |
-
dropout = kwargs.get("dropout", 0.1)
|
| 217 |
-
config = Kohonen4DConfig(
|
| 218 |
-
n_neurons=8,
|
| 219 |
-
grid_shape=(2, 2, 2, 1),
|
| 220 |
-
d_cache=d_cache,
|
| 221 |
-
d_model_in=d_model_in,
|
| 222 |
-
nhead=nhead,
|
| 223 |
-
d_ff=d_ff,
|
| 224 |
-
dropout=dropout,
|
| 225 |
-
)
|
| 226 |
-
self.cfg = config
|
| 227 |
-
assert self.cfg.n_neurons == 8, "V6 Kohonen 4D requer 8 neurônios (grid 2x2x2x1)"
|
| 228 |
-
assert math.prod(self.cfg.grid_shape) == 8, "grid_shape deve ter produto 8"
|
| 229 |
-
|
| 230 |
-
# ───────────────────────────────────────────────────────────────────
|
| 231 |
-
# Pesos do mapa: cache aprendível + proj_in
|
| 232 |
-
# ───────────────────────────────────────────────────────────────────
|
| 233 |
-
# Cada neurônio tem:
|
| 234 |
-
# - Um vetor de cache (d_cache) — seu "peso próprio" no espaço semântico
|
| 235 |
-
# - Uma projeção de entrada (d_model_in → d_cache)
|
| 236 |
-
self.neuron_cache = nn.Parameter(
|
| 237 |
-
torch.randn(self.cfg.n_neurons, self.cfg.d_cache) * 0.02
|
| 238 |
-
)
|
| 239 |
-
self.proj_in = nn.Linear(self.cfg.d_model_in, self.cfg.d_cache)
|
| 240 |
-
|
| 241 |
-
# ───────────────────────────────────────────────────────────────────
|
| 242 |
-
# Dimensão semântica: TransformerUnit (cross-attention com 8 neurônios)
|
| 243 |
-
# ───────────────────────────────────────────────────────────────────
|
| 244 |
-
# Em vez de computar a distância euclidiana ||x - w_i|| no espaço
|
| 245 |
-
# original, usamos a atenção do TransformerUnit como uma função de
|
| 246 |
-
# vizinhança APRENDÍVEL. Cada neurônio é uma posição (1 das 8) na
|
| 247 |
-
# sequência de attention. A atenção calcula score(x, neuron_i).
|
| 248 |
-
# Como o TransformerUnit compartilha parâmetros, todos os 8 neurônios
|
| 249 |
-
# usam o mesmo Q,K,V proj — a diferenciação entre neurônios vem do
|
| 250 |
-
# neuron_cache (que é o valor aprendido individualmente por neurônio).
|
| 251 |
-
self.cross_attn = nn.MultiheadAttention(
|
| 252 |
-
self.cfg.d_cache, self.cfg.nhead, dropout=self.cfg.dropout, batch_first=True
|
| 253 |
-
)
|
| 254 |
-
|
| 255 |
-
# FFN pre-norm (consistente com TransformerUnit)
|
| 256 |
-
self.linear1 = nn.Linear(self.cfg.d_cache, self.cfg.d_ff)
|
| 257 |
-
self.linear2 = nn.Linear(self.cfg.d_ff, self.cfg.d_cache)
|
| 258 |
-
self.norm1 = nn.LayerNorm(self.cfg.d_cache)
|
| 259 |
-
self.norm2 = nn.LayerNorm(self.cfg.d_cache)
|
| 260 |
-
self.dropout1 = nn.Dropout(self.cfg.dropout)
|
| 261 |
-
self.dropout2 = nn.Dropout(self.cfg.dropout)
|
| 262 |
-
self.dropout_ff = nn.Dropout(self.cfg.dropout)
|
| 263 |
-
|
| 264 |
-
# ───────────────────────────────────────────────────────────────────
|
| 265 |
-
# Grid 4D e distâncias pré-computadas (não aprendíveis)
|
| 266 |
-
# ───────────────────────────────────────────────────────────────────
|
| 267 |
-
grid_coords = _build_grid_coords(self.cfg.grid_shape) # (8, 4)
|
| 268 |
-
grid_dist = _pairwise_grid_distances(grid_coords) # (8, 8)
|
| 269 |
-
self.register_buffer("grid_coords", grid_coords, persistent=False)
|
| 270 |
-
self.register_buffer("grid_dist", grid_dist, persistent=False)
|
| 271 |
-
|
| 272 |
-
# ───────────────────────────────────────────────────────────────────
|
| 273 |
-
# Estado do SOM (não-parametros, mas monitorável)
|
| 274 |
-
# ───────────────────────────────────────────────────────────────────
|
| 275 |
-
# step_t é atualizado por update_step() ou passado em forward()
|
| 276 |
-
self.register_buffer("step_t", torch.tensor(0, dtype=torch.long), persistent=False)
|
| 277 |
-
self.register_buffer("sigma_t", torch.tensor(self.cfg.sigma_0, dtype=torch.float32),
|
| 278 |
-
persistent=False)
|
| 279 |
-
self.register_buffer("alpha_t", torch.tensor(self.cfg.alpha_0, dtype=torch.float32),
|
| 280 |
-
persistent=False)
|
| 281 |
-
|
| 282 |
-
# ───────────────────────────────────────────────────────────────────
|
| 283 |
-
# V6.1 — neuron_filled_mask: marca quais neurônios já receberam
|
| 284 |
-
# update SOM (BMU + vizinhos). Inicialmente todos False (mapa vazio).
|
| 285 |
-
# Usado por:
|
| 286 |
-
# - pgvector_lookup(): distingue COMPLETE (filled + sim alta)
|
| 287 |
-
# de INCOMPLETE (sim alta mas não filled)
|
| 288 |
-
# - EWC V6.1: só computa Fisher para neurônios filled — não
|
| 289 |
-
# "reutiliza" pesos que o SOM já tem (evita dupla penalização)
|
| 290 |
-
# ───────────────────────────────────────────────────────────────────
|
| 291 |
-
self.register_buffer(
|
| 292 |
-
"neuron_filled_mask",
|
| 293 |
-
torch.zeros(self.cfg.n_neurons, dtype=torch.bool),
|
| 294 |
-
persistent=False,
|
| 295 |
-
)
|
| 296 |
-
|
| 297 |
-
# Histórico de BMU (para monitoramento)
|
| 298 |
-
self.bmu_history: List[int] = []
|
| 299 |
-
self.bmu_history_max = 1000 # limita crescimento
|
| 300 |
-
|
| 301 |
-
# =====================================================================
|
| 302 |
-
# Schedule do SOM (σ e α)
|
| 303 |
-
# =====================================================================
|
| 304 |
-
def _schedule_sigma(self, t: int) -> float:
|
| 305 |
-
"""σ_t = σ_0 * (σ_f/σ_0)^(t/T) — decaimento exponencial."""
|
| 306 |
-
T = max(1, self.cfg.total_steps)
|
| 307 |
-
t = max(0, min(t, T))
|
| 308 |
-
ratio = self.cfg.sigma_f / max(self.cfg.sigma_0, 1e-8)
|
| 309 |
-
return self.cfg.sigma_0 * (ratio ** (t / T))
|
| 310 |
-
|
| 311 |
-
def _schedule_alpha(self, t: int) -> float:
|
| 312 |
-
"""α_t = α_0 * (α_f/α_0)^(t/T) — decaimento exponencial."""
|
| 313 |
-
T = max(1, self.cfg.total_steps)
|
| 314 |
-
t = max(0, min(t, T))
|
| 315 |
-
ratio = self.cfg.alpha_f / max(self.cfg.alpha_0, 1e-8)
|
| 316 |
-
return self.cfg.alpha_0 * (ratio ** (t / T))
|
| 317 |
-
|
| 318 |
-
def update_step(self, t: int) -> None:
|
| 319 |
-
"""Atualiza step_t e recalcula sigma_t e alpha_t.
|
| 320 |
-
|
| 321 |
-
Chamar a cada step de treino para manter o schedule.
|
| 322 |
-
"""
|
| 323 |
-
self.step_t.fill_(t)
|
| 324 |
-
self.sigma_t.fill_(self._schedule_sigma(t))
|
| 325 |
-
self.alpha_t.fill_(self._schedule_alpha(t))
|
| 326 |
-
|
| 327 |
-
# =====================================================================
|
| 328 |
-
# Função de vizinhança Gaussiana
|
| 329 |
-
# =====================================================================
|
| 330 |
-
def _neighborhood(self, bmu_idx: int, sigma: float) -> torch.Tensor:
|
| 331 |
-
"""Λ(d_bmu, σ) = exp(-d²/(2σ²)) para todos os neurônios.
|
| 332 |
-
|
| 333 |
-
Args:
|
| 334 |
-
bmu_idx: índice do BMU
|
| 335 |
-
sigma: raio de vizinhança atual
|
| 336 |
-
|
| 337 |
-
Returns: (n_neurons,) — peso de cada neurônio (1.0 no BMU, decaindo)
|
| 338 |
-
"""
|
| 339 |
-
d = self.grid_dist[bmu_idx] # (n_neurons,)
|
| 340 |
-
return torch.exp(-(d ** 2) / (2.0 * sigma * sigma + 1e-12))
|
| 341 |
-
|
| 342 |
-
# =====================================================================
|
| 343 |
-
# Compatibilidade de shape (dados não preparados para Kohonen 4D)
|
| 344 |
-
# =====================================================================
|
| 345 |
-
def _ensure_neuron_compat(self, H: torch.Tensor) -> torch.Tensor:
|
| 346 |
-
"""Garante que H tem dimensão compatível com d_model_in.
|
| 347 |
-
|
| 348 |
-
Se H não tem a dimensão esperada pelo mapa (datasets não preparados
|
| 349 |
-
para Kohonen 4D), faz pad/resize com valores aleatórios controlados.
|
| 350 |
-
"""
|
| 351 |
-
if H.size(-1) == self.cfg.d_model_in:
|
| 352 |
-
return H
|
| 353 |
-
# Projeta H para d_model_in via pad/truncate com ruído controlado
|
| 354 |
-
target = self.cfg.d_model_in
|
| 355 |
-
current = H.size(-1)
|
| 356 |
-
if current < target:
|
| 357 |
-
# Pad com ruído gaussiano de magnitude pequena
|
| 358 |
-
pad_shape = H.shape[:-1] + (target - current,)
|
| 359 |
-
pad = torch.randn(pad_shape, device=H.device, dtype=H.dtype) * self.cfg.random_pad_scale
|
| 360 |
-
return torch.cat([H, pad], dim=-1)
|
| 361 |
-
else:
|
| 362 |
-
# Truncate (preserva os primeiros target componentes)
|
| 363 |
-
return H[..., :target]
|
| 364 |
-
|
| 365 |
-
def map_contains_info(self, x: torch.Tensor) -> torch.Tensor:
|
| 366 |
-
"""Verifica se o mapa já possui a informação de x (batch-wise).
|
| 367 |
-
|
| 368 |
-
Computa a similaridade entre x (projetado) e cada neurônio do mapa.
|
| 369 |
-
Se max sim < threshold, considera que o mapa não tem aquela info.
|
| 370 |
-
|
| 371 |
-
Args:
|
| 372 |
-
x: (batch, d_model_in) ou (batch, K, d_model_in)
|
| 373 |
-
|
| 374 |
-
Returns: (batch,) — booleno: True se o mapa tem info de x
|
| 375 |
-
"""
|
| 376 |
-
x = self._ensure_neuron_compat(x)
|
| 377 |
-
if x.dim() == 3:
|
| 378 |
-
# Pool sobre K módulos
|
| 379 |
-
x = x.mean(dim=1)
|
| 380 |
-
x_proj = self.proj_in(x) # (batch, d_cache)
|
| 381 |
-
# Similaridade = cosseno entre x_proj e cada neuron_cache
|
| 382 |
-
x_norm = F.normalize(x_proj, dim=-1) # (batch, d_cache)
|
| 383 |
-
c_norm = F.normalize(self.neuron_cache, dim=-1) # (8, d_cache)
|
| 384 |
-
sims = x_norm @ c_norm.T # (batch, 8)
|
| 385 |
-
max_sim, _ = sims.max(dim=-1) # (batch,)
|
| 386 |
-
# Threshold conservador: 0.5 (similaridade cosseno)
|
| 387 |
-
return max_sim >= 0.5
|
| 388 |
-
|
| 389 |
-
# =====================================================================
|
| 390 |
-
# V6.1 — PGVector-like lookup
|
| 391 |
-
# =====================================================================
|
| 392 |
-
def pgvector_lookup(self, x: torch.Tensor) -> Dict[str, Any]:
|
| 393 |
-
"""Busca PGVector-like: verifica se o mapa SOM tem a info de x.
|
| 394 |
-
|
| 395 |
-
Distingue três estados (conforme requisitado pelo usuário):
|
| 396 |
-
|
| 397 |
-
COMPLETE : existe um neurônio com sim ≥ pgvector_complete_thr
|
| 398 |
-
E esse neurônio está "filled" (neuron_filled_mask=True)
|
| 399 |
-
→ info presente e consolidada pelo SOM + EWC
|
| 400 |
-
INCOMPLETE : existe um neurônio com sim ≥ pgvector_missing_thr,
|
| 401 |
-
mas NÃO está filled (faltam elementos — pesos EWC
|
| 402 |
-
ainda não foram computados para aquela região do mapa)
|
| 403 |
-
→ info parcialmente presente; o EWC deve completar
|
| 404 |
-
MISSING : nenhum neurônio tem sim ≥ pgvector_missing_thr
|
| 405 |
-
→ info ausente; usar EWC com pesos aleatórios iniciais
|
| 406 |
-
|
| 407 |
-
O usuário observou: "em uma busca PGVector posso encontrar informações
|
| 408 |
-
incompletas (faltam elementos e no caso os pesos que devem ser
|
| 409 |
-
calculados por EWC) e completas". Este método implementa essa triagem.
|
| 410 |
-
|
| 411 |
-
Args:
|
| 412 |
-
x: (batch, d_model_in) ou (batch, K, d_model_in)
|
| 413 |
-
|
| 414 |
-
Returns: dict com:
|
| 415 |
-
- status: str — "COMPLETE" | "INCOMPLETE" | "MISSING"
|
| 416 |
-
(status do BMU do batch; se batch misto, usa o pior caso)
|
| 417 |
-
- status_per_sample: List[str] — status por sample do batch
|
| 418 |
-
- best_neuron_idx: int — índice do neurônio mais similar (média)
|
| 419 |
-
- best_sim: float — similaridade máxima (média do batch)
|
| 420 |
-
- coverage_ratio: float — fração de neurônios com sim ≥ missing_thr
|
| 421 |
-
- filled_ratio: float — fração de neurônios filled no mapa
|
| 422 |
-
- missing_neurons: List[int] — neurônios ainda não filled
|
| 423 |
-
- n_filled: int — número de neurônios filled
|
| 424 |
-
"""
|
| 425 |
-
x = self._ensure_neuron_compat(x)
|
| 426 |
-
if x.dim() == 3:
|
| 427 |
-
x = x.mean(dim=1)
|
| 428 |
-
x_proj = self.proj_in(x) # (batch, d_cache)
|
| 429 |
-
x_norm = F.normalize(x_proj, dim=-1)
|
| 430 |
-
c_norm = F.normalize(self.neuron_cache, dim=-1)
|
| 431 |
-
sims = x_norm @ c_norm.T # (batch, 8)
|
| 432 |
-
max_sim, best_idx = sims.max(dim=-1) # (batch,), (batch,)
|
| 433 |
-
|
| 434 |
-
# Status por sample
|
| 435 |
-
statuses: List[str] = []
|
| 436 |
-
for i in range(x.size(0)):
|
| 437 |
-
sim_i = float(max_sim[i].item())
|
| 438 |
-
bmu_i = int(best_idx[i].item())
|
| 439 |
-
if sim_i < self.cfg.pgvector_missing_thr:
|
| 440 |
-
statuses.append("MISSING")
|
| 441 |
-
elif sim_i >= self.cfg.pgvector_complete_thr and bool(self.neuron_filled_mask[bmu_i]):
|
| 442 |
-
statuses.append("COMPLETE")
|
| 443 |
-
else:
|
| 444 |
-
# entre os thresholds OU sim alta mas não filled
|
| 445 |
-
statuses.append("INCOMPLETE")
|
| 446 |
-
|
| 447 |
-
# Status do batch: pior caso (MISSING > INCOMPLETE > COMPLETE)
|
| 448 |
-
if "MISSING" in statuses:
|
| 449 |
-
batch_status = "MISSING"
|
| 450 |
-
elif "INCOMPLETE" in statuses:
|
| 451 |
-
batch_status = "INCOMPLETE"
|
| 452 |
-
else:
|
| 453 |
-
batch_status = "COMPLETE"
|
| 454 |
-
|
| 455 |
-
# Coverage ratio: fração de neurônios com sim ≥ missing_thr (média do batch)
|
| 456 |
-
coverage = float((sims >= self.cfg.pgvector_missing_thr).float().mean().item())
|
| 457 |
-
filled_ratio = float(self.neuron_filled_mask.float().mean().item())
|
| 458 |
-
missing_neurons = [
|
| 459 |
-
i for i in range(self.cfg.n_neurons)
|
| 460 |
-
if not bool(self.neuron_filled_mask[i])
|
| 461 |
-
]
|
| 462 |
-
|
| 463 |
-
return {
|
| 464 |
-
"status": batch_status,
|
| 465 |
-
"status_per_sample": statuses,
|
| 466 |
-
"best_neuron_idx": int(best_idx.float().mean().item()),
|
| 467 |
-
"best_sim": float(max_sim.mean().item()),
|
| 468 |
-
"coverage_ratio": coverage,
|
| 469 |
-
"filled_ratio": filled_ratio,
|
| 470 |
-
"missing_neurons": missing_neurons,
|
| 471 |
-
"n_filled": int(self.neuron_filled_mask.sum().item()),
|
| 472 |
-
}
|
| 473 |
-
|
| 474 |
-
def fill_missing_neurons_random(self, scale: Optional[float] = None) -> int:
|
| 475 |
-
"""V6.1: preenche neurônios não-filled com valores aleatórios.
|
| 476 |
-
|
| 477 |
-
Conforme user requirement: "se não houver atribuir valores aleatórios
|
| 478 |
-
aos parâmetros faltantes". Este método é chamado pelos níveis de
|
| 479 |
-
treinamento e hipótese quando pgvector_lookup retorna INCOMPLETE/MISSING
|
| 480 |
-
e o usuário quer garantir que o mapa tenha algo (mesmo aleatório) em
|
| 481 |
-
todas as posições — para que o forward não degenere.
|
| 482 |
-
|
| 483 |
-
Args:
|
| 484 |
-
scale: magnitude dos valores aleatórios (default: cfg.random_pad_scale)
|
| 485 |
-
|
| 486 |
-
Returns:
|
| 487 |
-
nº de neurônios preenchidos
|
| 488 |
-
"""
|
| 489 |
-
if scale is None:
|
| 490 |
-
scale = self.cfg.random_pad_scale
|
| 491 |
-
n_filled_now = 0
|
| 492 |
-
with torch.no_grad():
|
| 493 |
-
for i in range(self.cfg.n_neurons):
|
| 494 |
-
if not bool(self.neuron_filled_mask[i]):
|
| 495 |
-
# Atribui valores aleatórios controlados ao cache do neurônio
|
| 496 |
-
self.neuron_cache[i].add_(
|
| 497 |
-
torch.randn_like(self.neuron_cache[i]) * scale
|
| 498 |
-
)
|
| 499 |
-
self.neuron_filled_mask[i] = True
|
| 500 |
-
n_filled_now += 1
|
| 501 |
-
return n_filled_now
|
| 502 |
-
|
| 503 |
-
def get_filled_neurons(self) -> List[int]:
|
| 504 |
-
"""V6.1: retorna lista de índices de neurônios filled."""
|
| 505 |
-
return [i for i in range(self.cfg.n_neurons)
|
| 506 |
-
if bool(self.neuron_filled_mask[i])]
|
| 507 |
-
|
| 508 |
-
# =====================================================================
|
| 509 |
-
# Best Matching Unit
|
| 510 |
-
# =====================================================================
|
| 511 |
-
def _find_bmu(self, x_proj: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 512 |
-
"""Encontra o BMU para cada sample do batch.
|
| 513 |
-
|
| 514 |
-
Args:
|
| 515 |
-
x_proj: (batch, d_cache) — entrada já projetada
|
| 516 |
-
|
| 517 |
-
Returns:
|
| 518 |
-
bmu_idx: (batch,) — índice do neurônio vencedor
|
| 519 |
-
bmu_sim: (batch,) — similaridade do BMU (para monitoramento)
|
| 520 |
-
"""
|
| 521 |
-
# Similaridade cosseno entre x e cada neurônio (alta = match)
|
| 522 |
-
x_norm = F.normalize(x_proj, dim=-1)
|
| 523 |
-
c_norm = F.normalize(self.neuron_cache, dim=-1)
|
| 524 |
-
sims = x_norm @ c_norm.T / self.cfg.bmu_temp # (batch, 8)
|
| 525 |
-
bmu_sim, bmu_idx = sims.max(dim=-1)
|
| 526 |
-
return bmu_idx, bmu_sim
|
| 527 |
-
|
| 528 |
-
# =====================================================================
|
| 529 |
-
# Forward principal
|
| 530 |
-
# =====================================================================
|
| 531 |
-
def forward(
|
| 532 |
-
self,
|
| 533 |
-
H: torch.Tensor,
|
| 534 |
-
step: Optional[int] = None,
|
| 535 |
-
return_aux: bool = False,
|
| 536 |
-
) -> torch.Tensor:
|
| 537 |
-
"""H: (batch, K, d_model_in) → o: (batch, d_cache).
|
| 538 |
-
|
| 539 |
-
Args:
|
| 540 |
-
H: (batch, K, d_model_in) — K saídas dos K módulos u8cell_T
|
| 541 |
-
step: passo global de treino (None = usar step_t interno)
|
| 542 |
-
return_aux: se True, retorna (o, aux_dict) com métricas V6
|
| 543 |
-
|
| 544 |
-
Returns:
|
| 545 |
-
o: (batch, d_cache) — saída agregada do orquestrador
|
| 546 |
-
aux_dict (se return_aux): sigma_t, alpha_t, bmu_id, etc.
|
| 547 |
-
"""
|
| 548 |
-
# 1. Garantir compatibilidade de shape (datasets não preparados p/ Kohonen 4D)
|
| 549 |
-
H = self._ensure_neuron_compat(H)
|
| 550 |
-
batch_size, K, _ = H.shape
|
| 551 |
-
|
| 552 |
-
# 2. Atualizar schedule do SOM
|
| 553 |
-
if step is not None:
|
| 554 |
-
self.update_step(step)
|
| 555 |
-
sigma_t = float(self.sigma_t.item())
|
| 556 |
-
alpha_t = float(self.alpha_t.item())
|
| 557 |
-
|
| 558 |
-
# 3. Projeta H para o espaço do cache: (batch, K, d_cache)
|
| 559 |
-
H_proj = self.proj_in(H)
|
| 560 |
-
|
| 561 |
-
# 4. Pool de H_proj para encontrar o BMU: (batch, d_cache)
|
| 562 |
-
H_pooled = H_proj.mean(dim=1)
|
| 563 |
-
|
| 564 |
-
# 5. Encontrar BMU por sample
|
| 565 |
-
bmu_idx, bmu_sim = self._find_bmu(H_pooled) # (batch,), (batch,)
|
| 566 |
-
|
| 567 |
-
# 6. Calcular peso de vizinhança para cada sample e neurônio
|
| 568 |
-
# Λ(d_bmu, σ) — para cada sample, pega a linha do BMU no grid_dist
|
| 569 |
-
# bmu_idx: (batch,) → neighborhood: (batch, n_neurons)
|
| 570 |
-
bmu_indices = bmu_idx.tolist()
|
| 571 |
-
neighborhoods = torch.stack(
|
| 572 |
-
[self._neighborhood(b, sigma_t) for b in bmu_indices], dim=0
|
| 573 |
-
) # (batch, n_neurons)
|
| 574 |
-
|
| 575 |
-
# 7. Cross-attention (dimensão semântica do TransformerUnit)
|
| 576 |
-
# queries = neuron_cache expandido, keys/values = H_proj
|
| 577 |
-
# Isso substitui a simples distância euclidiana por uma função
|
| 578 |
-
# de vizinhança APRENDÍVEL via attention.
|
| 579 |
-
cache = self.neuron_cache.unsqueeze(0).expand(batch_size, -1, -1) # (batch, 8, d_cache)
|
| 580 |
-
normed_cache = self.norm1(cache)
|
| 581 |
-
normed_H = self.norm1(H_proj)
|
| 582 |
-
attn_out, _ = self.cross_attn(normed_cache, normed_H, normed_H, need_weights=False)
|
| 583 |
-
cache = cache + self.dropout1(attn_out)
|
| 584 |
-
|
| 585 |
-
# FFN pre-norm
|
| 586 |
-
normed = self.norm2(cache)
|
| 587 |
-
ff_out = self.linear2(self.dropout_ff(F.relu(self.linear1(normed))))
|
| 588 |
-
cache = cache + self.dropout2(ff_out) # (batch, 8, d_cache)
|
| 589 |
-
|
| 590 |
-
# 8. Atualizar pesos do mapa SOM (apenas em treino)
|
| 591 |
-
# NOTA: O update SOM é feito APÓS backward (em update_map_after_backward)
|
| 592 |
-
# para não quebrar o autograd graph. Aqui apenas armazenamos o estado
|
| 593 |
-
# necessário (H_pooled, neighborhoods, bmu_idx) para o update.
|
| 594 |
-
# Em eval mode, o mapa está congelado (sem update).
|
| 595 |
-
if self.training:
|
| 596 |
-
# Cache do estado para update posterior
|
| 597 |
-
self._pending_update = (H_pooled.detach(), neighborhoods.detach(), bmu_idx.detach())
|
| 598 |
-
update_metrics = {
|
| 599 |
-
"update_magnitude": 0.0, # será preenchido em update_map_after_backward
|
| 600 |
-
"bmu_drift": 0.0,
|
| 601 |
-
"map_frozen": False,
|
| 602 |
-
}
|
| 603 |
-
else:
|
| 604 |
-
self._pending_update = None
|
| 605 |
-
update_metrics = {
|
| 606 |
-
"update_magnitude": 0.0,
|
| 607 |
-
"bmu_drift": 0.0,
|
| 608 |
-
"map_frozen": True,
|
| 609 |
-
}
|
| 610 |
-
|
| 611 |
-
# 9. Saída agregada: combina BMU + média ponderada por Λ + cache attention
|
| 612 |
-
# Peso final do neurônio i = Λ(d_i_bmu, σ) normalizado
|
| 613 |
-
nh_norm = neighborhoods / (neighborhoods.sum(dim=-1, keepdim=True) + 1e-8) # (batch, 8)
|
| 614 |
-
# Saída = soma ponderada do cache attention pelos pesos de vizinhança
|
| 615 |
-
o = (cache * nh_norm.unsqueeze(-1)).sum(dim=1) # (batch, d_cache)
|
| 616 |
-
|
| 617 |
-
# 10. Atualiza histórico de BMU (para monitoramento)
|
| 618 |
-
if len(self.bmu_history) < self.bmu_history_max:
|
| 619 |
-
self.bmu_history.extend(bmu_indices[:min(8, batch_size)])
|
| 620 |
-
|
| 621 |
-
if not return_aux:
|
| 622 |
-
return o
|
| 623 |
-
|
| 624 |
-
# ───────────────────────────────────────────────────────────────────
|
| 625 |
-
# Métricas V6 para monitoramento
|
| 626 |
-
# ───────────────────────────────────────────────────────────────────
|
| 627 |
-
# BMU diversity: nº de BMUs distintos no batch
|
| 628 |
-
unique_bmu = len(set(bmu_indices))
|
| 629 |
-
# Neighborhood spread: média de Λ sobre o batch e neurônios
|
| 630 |
-
spread = float(neighborhoods.mean().item())
|
| 631 |
-
# Map entropy: entropia da distribuição de BMU no batch
|
| 632 |
-
bmu_counts = torch.zeros(self.cfg.n_neurons, device=bmu_idx.device)
|
| 633 |
-
for b in bmu_indices:
|
| 634 |
-
bmu_counts[b] += 1
|
| 635 |
-
bmu_probs = bmu_counts / (bmu_counts.sum() + 1e-8)
|
| 636 |
-
map_entropy = float(
|
| 637 |
-
-(bmu_probs * (bmu_probs + 1e-8).log()).sum().item()
|
| 638 |
-
)
|
| 639 |
-
|
| 640 |
-
aux = {
|
| 641 |
-
"sigma_t": sigma_t,
|
| 642 |
-
"alpha_t": alpha_t,
|
| 643 |
-
"step_t": int(self.step_t.item()),
|
| 644 |
-
"bmu_id_mean": float(bmu_idx.float().mean().item()),
|
| 645 |
-
"bmu_diversity": unique_bmu,
|
| 646 |
-
"bmu_sim_mean": float(bmu_sim.mean().item()),
|
| 647 |
-
"neighborhood_spread": spread,
|
| 648 |
-
"map_entropy": map_entropy,
|
| 649 |
-
"neighborhoods_sample": neighborhoods[0].detach().cpu().tolist(),
|
| 650 |
-
"n_filled": int(self.neuron_filled_mask.sum().item()),
|
| 651 |
-
"filled_ratio": float(self.neuron_filled_mask.float().mean().item()),
|
| 652 |
-
**update_metrics,
|
| 653 |
-
}
|
| 654 |
-
return o, aux
|
| 655 |
-
|
| 656 |
-
# =====================================================================
|
| 657 |
-
# Atualização SOM (neurônio vencedor + vizinhos) — chamada APÓS backward
|
| 658 |
-
# =====================================================================
|
| 659 |
-
def update_map_after_backward(self, alpha_t: Optional[float] = None) -> Dict[str, float]:
|
| 660 |
-
"""Aplica o update SOM nos pesos do mapa. Chamar APÓS loss.backward().
|
| 661 |
-
|
| 662 |
-
Implementa a regra de Kohonen:
|
| 663 |
-
w_i ← w_i + α · Λ(d_i_bmu, σ) · (x - w_i)
|
| 664 |
-
|
| 665 |
-
Esta função DEVE ser chamada depois do backward pass, com
|
| 666 |
-
torch.no_grad() implícito, para não interferir com o autograd graph.
|
| 667 |
-
|
| 668 |
-
Args:
|
| 669 |
-
alpha_t: taxa de aprendizado (default: usar alpha_t interno)
|
| 670 |
-
|
| 671 |
-
Returns:
|
| 672 |
-
Dict com métricas da atualização
|
| 673 |
-
"""
|
| 674 |
-
if not hasattr(self, "_pending_update") or self._pending_update is None:
|
| 675 |
-
return {"update_magnitude": 0.0, "bmu_drift": 0.0, "map_frozen": True}
|
| 676 |
-
|
| 677 |
-
H_pooled, neighborhoods, bmu_idx = self._pending_update
|
| 678 |
-
if alpha_t is None:
|
| 679 |
-
alpha_t = float(self.alpha_t.item())
|
| 680 |
-
|
| 681 |
-
with torch.no_grad():
|
| 682 |
-
# Δw_i = α · mean_batch( Λ_i · (x - w_i) )
|
| 683 |
-
x_exp = H_pooled.unsqueeze(1) # (batch, 1, d_cache)
|
| 684 |
-
w_exp = self.neuron_cache.unsqueeze(0) # (1, n_neurons, d_cache)
|
| 685 |
-
diff = x_exp - w_exp # (batch, n_neurons, d_cache)
|
| 686 |
-
nh = neighborhoods.unsqueeze(-1) # (batch, n_neurons, 1)
|
| 687 |
-
delta = (nh * diff).mean(dim=0) # (n_neurons, d_cache)
|
| 688 |
-
update_magnitude = float(delta.norm().item())
|
| 689 |
-
old_cache_norm = float(self.neuron_cache.norm().item())
|
| 690 |
-
self.neuron_cache.add_(alpha_t * delta)
|
| 691 |
-
new_cache_norm = float(self.neuron_cache.norm().item())
|
| 692 |
-
bmu_drift = abs(new_cache_norm - old_cache_norm)
|
| 693 |
-
|
| 694 |
-
# V6.1 — marca neurônios "filled": BMU + vizinhos com Λ > 0.1
|
| 695 |
-
# (próximo o suficiente do BMU para terem sido atualizados de
|
| 696 |
-
# forma significativa). Isto é usado por:
|
| 697 |
-
# - pgvector_lookup(): distingue COMPLETE de INCOMPLETE
|
| 698 |
-
# - EWC V6.1: só computa Fisher para neurônios filled
|
| 699 |
-
# O usuário pediu: "cada neurônio da grade tem peso próprio" —
|
| 700 |
-
# então cada neurônio preenchido individualmente.
|
| 701 |
-
mean_nh = neighborhoods.mean(dim=0) # (n_neurons,)
|
| 702 |
-
newly_filled_mask = mean_nh > 0.1
|
| 703 |
-
self.neuron_filled_mask = self.neuron_filled_mask | newly_filled_mask
|
| 704 |
-
|
| 705 |
-
# Clear pending update
|
| 706 |
-
self._pending_update = None
|
| 707 |
-
return {
|
| 708 |
-
"update_magnitude": update_magnitude,
|
| 709 |
-
"bmu_drift": bmu_drift,
|
| 710 |
-
"map_frozen": False,
|
| 711 |
-
"n_filled": int(self.neuron_filled_mask.sum().item()),
|
| 712 |
-
"newly_filled": int(newly_filled_mask.sum().item()),
|
| 713 |
-
}
|
| 714 |
-
|
| 715 |
-
# =====================================================================
|
| 716 |
-
# Utilitário para EWC (V6)
|
| 717 |
-
# =====================================================================
|
| 718 |
-
def get_neuron_params(self, neuron_idx: int) -> Dict[str, torch.Tensor]:
|
| 719 |
-
"""Retorna os parâmetros associados a um neurônio específico.
|
| 720 |
-
|
| 721 |
-
Usado pelo EWC V6 para computar Fisher Information por neurônio
|
| 722 |
-
(cada neurônio tem peso próprio — o usuário observou isso).
|
| 723 |
-
"""
|
| 724 |
-
if neuron_idx < 0 or neuron_idx >= self.cfg.n_neurons:
|
| 725 |
-
raise IndexError(f"neuron_idx {neuron_idx} fora do range [0, {self.cfg.n_neurons})")
|
| 726 |
-
return {
|
| 727 |
-
"cache": self.neuron_cache[neuron_idx],
|
| 728 |
-
# O proj_in e cross_attn são compartilhados entre neurônios,
|
| 729 |
-
# mas cada neurônio tem seu próprio cache. O EWC deve penalizar
|
| 730 |
-
# a mudança no cache de cada neurônio individualmente.
|
| 731 |
-
}
|
| 732 |
-
|
| 733 |
-
def get_per_neuron_fisher_targets(self) -> List[str]:
|
| 734 |
-
"""Lista de nomes de parâmetros do mapa (para EWC V6)."""
|
| 735 |
-
return ["neuron_cache"]
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
__all__ = ["OrqCell", "Kohonen4DConfig"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|