PowerMachine commited on
Commit
4e7f6ee
·
verified ·
1 Parent(s): f4432a5

Delete src/bigru_t/model/orq_cell.py

Browse files
Files changed (1) hide show
  1. 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"]